From 93fff1889a95030f649e0f34577aa822df3b7ca7 Mon Sep 17 00:00:00 2001 From: maltsev Date: Wed, 28 Mar 2012 17:37:50 +0600 Subject: [PATCH] Test commit --- .gitignore | 19 + libs/A3DModelsBase-2.5.1.swc | Bin 0 -> 200029 bytes libs/A3DModelsBase-2.5.2.swc | Bin 0 -> 199322 bytes libs/AlternativaProtocol-2.53.0.swc | Bin 0 -> 54153 bytes libs/OSGIBase.swc | Bin 0 -> 35575 bytes libs/ProtocolTypes.swc | Bin 0 -> 4903 bytes libs/apparat-ersatz-1.0-RC9.swc | Bin 0 -> 21930 bytes pom-standalone.xml | 56 + pom.xml | 57 + src/alternativa/Alternativa3D.as | 24 + src/alternativa/engine3d/alternativa3d.as | 13 + .../engine3d/animation/AnimationClip.as | 495 ++++++ .../engine3d/animation/AnimationController.as | 220 +++ .../engine3d/animation/AnimationCouple.as | 138 ++ .../engine3d/animation/AnimationNode.as | 96 ++ .../engine3d/animation/AnimationNotify.as | 78 + .../engine3d/animation/AnimationState.as | 262 +++ .../engine3d/animation/AnimationSwitcher.as | 198 +++ .../engine3d/animation/events/NotifyEvent.as | 44 + .../engine3d/animation/keys/Keyframe.as | 84 + .../engine3d/animation/keys/NumberKey.as | 73 + .../engine3d/animation/keys/NumberTrack.as | 160 ++ .../engine3d/animation/keys/Track.as | 261 +++ .../engine3d/animation/keys/TransformKey.as | 164 ++ .../engine3d/animation/keys/TransformTrack.as | 240 +++ .../engine3d/collisions/EllipsoidCollider.as | 576 +++++++ .../controllers/SimpleObjectController.as | 486 ++++++ src/alternativa/engine3d/core/BoundBox.as | 299 ++++ src/alternativa/engine3d/core/Camera3D.as | 1188 +++++++++++++ src/alternativa/engine3d/core/CullingPlane.as | 50 + src/alternativa/engine3d/core/Debug.as | 311 ++++ .../engine3d/core/DebugDrawUnit.as | 150 ++ .../engine3d/core/DebugMaterialsRenderer.as | 50 + src/alternativa/engine3d/core/DrawUnit.as | 248 +++ src/alternativa/engine3d/core/Light3D.as | 116 ++ src/alternativa/engine3d/core/Object3D.as | 1453 ++++++++++++++++ src/alternativa/engine3d/core/Occluder.as | 1386 +++++++++++++++ .../engine3d/core/RayIntersectionData.as | 59 + src/alternativa/engine3d/core/Renderer.as | 238 +++ .../core/RendererContext3DProperties.as | 23 + src/alternativa/engine3d/core/Resource.as | 57 + src/alternativa/engine3d/core/Transform3D.as | 269 +++ .../engine3d/core/VertexAttributes.as | 112 ++ src/alternativa/engine3d/core/VertexStream.as | 24 + src/alternativa/engine3d/core/View.as | 1432 ++++++++++++++++ .../engine3d/core/events/Event3D.as | 140 ++ .../engine3d/core/events/MouseEvent3D.as | 187 ++ .../engine3d/effects/AGALMiniAssembler.as | 724 ++++++++ src/alternativa/engine3d/effects/Particle.as | 63 + .../engine3d/effects/ParticleEffect.as | 183 ++ .../engine3d/effects/ParticlePrototype.as | 139 ++ .../engine3d/effects/ParticleSystem.as | 481 ++++++ .../engine3d/effects/TextureAtlas.as | 45 + .../engine3d/lights/AmbientLight.as | 64 + .../engine3d/lights/DirectionalLight.as | 67 + src/alternativa/engine3d/lights/OmniLight.as | 206 +++ src/alternativa/engine3d/lights/SpotLight.as | 211 +++ .../engine3d/loaders/ExporterA3D.as | 633 +++++++ .../engine3d/loaders/IIDGenerator.as | 23 + .../loaders/IncrementalIDGenerator.as | 38 + src/alternativa/engine3d/loaders/Parser.as | 1067 ++++++++++++ src/alternativa/engine3d/loaders/Parser3DS.as | 1499 +++++++++++++++++ src/alternativa/engine3d/loaders/ParserA3D.as | 188 +++ .../engine3d/loaders/ParserCollada.as | 391 +++++ .../engine3d/loaders/ParserMaterial.as | 107 ++ .../engine3d/loaders/ResourceLoader.as | 186 ++ .../engine3d/loaders/TexturesLoader.as | 328 ++++ .../engine3d/loaders/collada/DaeArray.as | 51 + .../engine3d/loaders/collada/DaeChannel.as | 213 +++ .../engine3d/loaders/collada/DaeController.as | 542 ++++++ .../engine3d/loaders/collada/DaeDocument.as | 248 +++ .../engine3d/loaders/collada/DaeEffect.as | 215 +++ .../loaders/collada/DaeEffectParam.as | 95 ++ .../engine3d/loaders/collada/DaeElement.as | 115 ++ .../engine3d/loaders/collada/DaeGeometry.as | 187 ++ .../engine3d/loaders/collada/DaeImage.as | 37 + .../engine3d/loaders/collada/DaeInput.as | 63 + .../loaders/collada/DaeInstanceController.as | 111 ++ .../loaders/collada/DaeInstanceMaterial.as | 49 + .../engine3d/loaders/collada/DaeLight.as | 119 ++ .../engine3d/loaders/collada/DaeLogger.as | 62 + .../engine3d/loaders/collada/DaeMaterial.as | 72 + .../engine3d/loaders/collada/DaeNode.as | 517 ++++++ .../engine3d/loaders/collada/DaeObject.as | 31 + .../engine3d/loaders/collada/DaeParam.as | 89 + .../engine3d/loaders/collada/DaePrimitive.as | 331 ++++ .../engine3d/loaders/collada/DaeSampler.as | 118 ++ .../engine3d/loaders/collada/DaeSource.as | 164 ++ .../engine3d/loaders/collada/DaeUnits.as | 26 + .../engine3d/loaders/collada/DaeVertex.as | 76 + .../loaders/collada/DaeVertexChannels.as | 29 + .../engine3d/loaders/collada/DaeVertices.as | 49 + .../loaders/collada/DaeVisualScene.as | 43 + .../engine3d/loaders/collada/collada.as | 17 + .../loaders/events/TexturesLoaderEvent.as | 69 + .../engine3d/materials/A3DUtils.as | 343 ++++ .../engine3d/materials/EnvironmentMaterial.as | 922 ++++++++++ .../engine3d/materials/FillMaterial.as | 153 ++ src/alternativa/engine3d/materials/FogMode.as | 24 + .../engine3d/materials/LightMapMaterial.as | 256 +++ .../engine3d/materials/Material.as | 116 ++ .../engine3d/materials/NormalMapSpace.as | 34 + .../engine3d/materials/ShaderProgram.as | 59 + .../engine3d/materials/StandardMaterial.as | 939 +++++++++++ .../engine3d/materials/TextureMaterial.as | 333 ++++ .../materials/VertexLightTextureMaterial.as | 357 ++++ .../materials/compiler/CommandType.as | 59 + .../materials/compiler/DestinationVariable.as | 71 + .../engine3d/materials/compiler/Linker.as | 423 +++++ .../engine3d/materials/compiler/Procedure.as | 487 ++++++ .../materials/compiler/RelativeVariable.as | 66 + .../materials/compiler/SamplerVariable.as | 99 ++ .../materials/compiler/SourceVariable.as | 104 ++ .../engine3d/materials/compiler/Variable.as | 71 + .../materials/compiler/VariableType.as | 55 + .../engine3d/objects/AnimSprite.as | 144 ++ .../engine3d/objects/AxisAlignedSprite.as | 273 +++ src/alternativa/engine3d/objects/Decal.as | 110 ++ src/alternativa/engine3d/objects/Joint.as | 126 ++ src/alternativa/engine3d/objects/LOD.as | 346 ++++ src/alternativa/engine3d/objects/Mesh.as | 195 +++ src/alternativa/engine3d/objects/MeshSet.as | 263 +++ src/alternativa/engine3d/objects/Skin.as | 733 ++++++++ src/alternativa/engine3d/objects/SkyBox.as | 328 ++++ src/alternativa/engine3d/objects/Sprite3D.as | 333 ++++ src/alternativa/engine3d/objects/Surface.as | 60 + src/alternativa/engine3d/objects/WireFrame.as | 409 +++++ src/alternativa/engine3d/primitives/Box.as | 283 ++++ .../engine3d/primitives/GeoSphere.as | 407 +++++ src/alternativa/engine3d/primitives/Plane.as | 202 +++ .../engine3d/resources/ATFTextureResource.as | 111 ++ .../resources/BitmapCubeTextureResource.as | 255 +++ .../resources/BitmapTextureResource.as | 133 ++ .../resources/ExternalTextureResource.as | 61 + .../engine3d/resources/Geometry.as | 1016 +++++++++++ .../engine3d/resources/TextureResource.as | 56 + .../engine3d/resources/WireGeometry.as | 189 +++ .../shadows/DirectionalLightShadow.as | 879 ++++++++++ .../shadows/DirectionalShadowRenderer.as | 540 ++++++ .../engine3d/shadows/OmniShadowRenderer.as | 798 +++++++++ .../OmniShadowRendererDebugMaterial.as | 162 ++ src/alternativa/engine3d/shadows/Shadow.as | 64 + .../engine3d/shadows/ShadowRenderer.as | 184 ++ .../engine3d/shadows/ShadowsSystem.as | 96 ++ .../engine3d/shadows/SpotShadowRenderer.as | 656 ++++++++ .../engine3d/shadows/StaticShadowRenderer.as | 569 +++++++ .../engine3d/utils/Object3DUtils.as | 92 + .../osgi/service/clientlog/IClientLog.as | 73 + .../clientlog/IClientLogChannelListener.as | 21 + 149 files changed, 37055 insertions(+) create mode 100644 .gitignore create mode 100644 libs/A3DModelsBase-2.5.1.swc create mode 100644 libs/A3DModelsBase-2.5.2.swc create mode 100644 libs/AlternativaProtocol-2.53.0.swc create mode 100644 libs/OSGIBase.swc create mode 100644 libs/ProtocolTypes.swc create mode 100644 libs/apparat-ersatz-1.0-RC9.swc create mode 100644 pom-standalone.xml create mode 100644 pom.xml create mode 100644 src/alternativa/Alternativa3D.as create mode 100644 src/alternativa/engine3d/alternativa3d.as create mode 100644 src/alternativa/engine3d/animation/AnimationClip.as create mode 100644 src/alternativa/engine3d/animation/AnimationController.as create mode 100644 src/alternativa/engine3d/animation/AnimationCouple.as create mode 100644 src/alternativa/engine3d/animation/AnimationNode.as create mode 100644 src/alternativa/engine3d/animation/AnimationNotify.as create mode 100644 src/alternativa/engine3d/animation/AnimationState.as create mode 100644 src/alternativa/engine3d/animation/AnimationSwitcher.as create mode 100644 src/alternativa/engine3d/animation/events/NotifyEvent.as create mode 100644 src/alternativa/engine3d/animation/keys/Keyframe.as create mode 100644 src/alternativa/engine3d/animation/keys/NumberKey.as create mode 100644 src/alternativa/engine3d/animation/keys/NumberTrack.as create mode 100644 src/alternativa/engine3d/animation/keys/Track.as create mode 100644 src/alternativa/engine3d/animation/keys/TransformKey.as create mode 100644 src/alternativa/engine3d/animation/keys/TransformTrack.as create mode 100644 src/alternativa/engine3d/collisions/EllipsoidCollider.as create mode 100644 src/alternativa/engine3d/controllers/SimpleObjectController.as create mode 100644 src/alternativa/engine3d/core/BoundBox.as create mode 100644 src/alternativa/engine3d/core/Camera3D.as create mode 100644 src/alternativa/engine3d/core/CullingPlane.as create mode 100644 src/alternativa/engine3d/core/Debug.as create mode 100644 src/alternativa/engine3d/core/DebugDrawUnit.as create mode 100644 src/alternativa/engine3d/core/DebugMaterialsRenderer.as create mode 100644 src/alternativa/engine3d/core/DrawUnit.as create mode 100644 src/alternativa/engine3d/core/Light3D.as create mode 100644 src/alternativa/engine3d/core/Object3D.as create mode 100644 src/alternativa/engine3d/core/Occluder.as create mode 100644 src/alternativa/engine3d/core/RayIntersectionData.as create mode 100644 src/alternativa/engine3d/core/Renderer.as create mode 100644 src/alternativa/engine3d/core/RendererContext3DProperties.as create mode 100644 src/alternativa/engine3d/core/Resource.as create mode 100644 src/alternativa/engine3d/core/Transform3D.as create mode 100644 src/alternativa/engine3d/core/VertexAttributes.as create mode 100644 src/alternativa/engine3d/core/VertexStream.as create mode 100644 src/alternativa/engine3d/core/View.as create mode 100644 src/alternativa/engine3d/core/events/Event3D.as create mode 100644 src/alternativa/engine3d/core/events/MouseEvent3D.as create mode 100644 src/alternativa/engine3d/effects/AGALMiniAssembler.as create mode 100644 src/alternativa/engine3d/effects/Particle.as create mode 100644 src/alternativa/engine3d/effects/ParticleEffect.as create mode 100644 src/alternativa/engine3d/effects/ParticlePrototype.as create mode 100644 src/alternativa/engine3d/effects/ParticleSystem.as create mode 100644 src/alternativa/engine3d/effects/TextureAtlas.as create mode 100644 src/alternativa/engine3d/lights/AmbientLight.as create mode 100644 src/alternativa/engine3d/lights/DirectionalLight.as create mode 100644 src/alternativa/engine3d/lights/OmniLight.as create mode 100644 src/alternativa/engine3d/lights/SpotLight.as create mode 100644 src/alternativa/engine3d/loaders/ExporterA3D.as create mode 100644 src/alternativa/engine3d/loaders/IIDGenerator.as create mode 100644 src/alternativa/engine3d/loaders/IncrementalIDGenerator.as create mode 100644 src/alternativa/engine3d/loaders/Parser.as create mode 100644 src/alternativa/engine3d/loaders/Parser3DS.as create mode 100644 src/alternativa/engine3d/loaders/ParserA3D.as create mode 100644 src/alternativa/engine3d/loaders/ParserCollada.as create mode 100644 src/alternativa/engine3d/loaders/ParserMaterial.as create mode 100644 src/alternativa/engine3d/loaders/ResourceLoader.as create mode 100644 src/alternativa/engine3d/loaders/TexturesLoader.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeArray.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeChannel.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeController.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeDocument.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeEffect.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeEffectParam.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeElement.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeGeometry.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeImage.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeInput.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeInstanceController.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeInstanceMaterial.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeLight.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeLogger.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeMaterial.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeNode.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeObject.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeParam.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaePrimitive.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeSampler.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeSource.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeUnits.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeVertex.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeVertexChannels.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeVertices.as create mode 100644 src/alternativa/engine3d/loaders/collada/DaeVisualScene.as create mode 100644 src/alternativa/engine3d/loaders/collada/collada.as create mode 100644 src/alternativa/engine3d/loaders/events/TexturesLoaderEvent.as create mode 100644 src/alternativa/engine3d/materials/A3DUtils.as create mode 100644 src/alternativa/engine3d/materials/EnvironmentMaterial.as create mode 100644 src/alternativa/engine3d/materials/FillMaterial.as create mode 100644 src/alternativa/engine3d/materials/FogMode.as create mode 100644 src/alternativa/engine3d/materials/LightMapMaterial.as create mode 100644 src/alternativa/engine3d/materials/Material.as create mode 100644 src/alternativa/engine3d/materials/NormalMapSpace.as create mode 100644 src/alternativa/engine3d/materials/ShaderProgram.as create mode 100644 src/alternativa/engine3d/materials/StandardMaterial.as create mode 100644 src/alternativa/engine3d/materials/TextureMaterial.as create mode 100644 src/alternativa/engine3d/materials/VertexLightTextureMaterial.as create mode 100644 src/alternativa/engine3d/materials/compiler/CommandType.as create mode 100644 src/alternativa/engine3d/materials/compiler/DestinationVariable.as create mode 100644 src/alternativa/engine3d/materials/compiler/Linker.as create mode 100644 src/alternativa/engine3d/materials/compiler/Procedure.as create mode 100644 src/alternativa/engine3d/materials/compiler/RelativeVariable.as create mode 100644 src/alternativa/engine3d/materials/compiler/SamplerVariable.as create mode 100644 src/alternativa/engine3d/materials/compiler/SourceVariable.as create mode 100644 src/alternativa/engine3d/materials/compiler/Variable.as create mode 100644 src/alternativa/engine3d/materials/compiler/VariableType.as create mode 100644 src/alternativa/engine3d/objects/AnimSprite.as create mode 100644 src/alternativa/engine3d/objects/AxisAlignedSprite.as create mode 100644 src/alternativa/engine3d/objects/Decal.as create mode 100644 src/alternativa/engine3d/objects/Joint.as create mode 100644 src/alternativa/engine3d/objects/LOD.as create mode 100644 src/alternativa/engine3d/objects/Mesh.as create mode 100644 src/alternativa/engine3d/objects/MeshSet.as create mode 100644 src/alternativa/engine3d/objects/Skin.as create mode 100644 src/alternativa/engine3d/objects/SkyBox.as create mode 100644 src/alternativa/engine3d/objects/Sprite3D.as create mode 100644 src/alternativa/engine3d/objects/Surface.as create mode 100644 src/alternativa/engine3d/objects/WireFrame.as create mode 100644 src/alternativa/engine3d/primitives/Box.as create mode 100644 src/alternativa/engine3d/primitives/GeoSphere.as create mode 100644 src/alternativa/engine3d/primitives/Plane.as create mode 100644 src/alternativa/engine3d/resources/ATFTextureResource.as create mode 100644 src/alternativa/engine3d/resources/BitmapCubeTextureResource.as create mode 100644 src/alternativa/engine3d/resources/BitmapTextureResource.as create mode 100644 src/alternativa/engine3d/resources/ExternalTextureResource.as create mode 100644 src/alternativa/engine3d/resources/Geometry.as create mode 100644 src/alternativa/engine3d/resources/TextureResource.as create mode 100644 src/alternativa/engine3d/resources/WireGeometry.as create mode 100644 src/alternativa/engine3d/shadows/DirectionalLightShadow.as create mode 100644 src/alternativa/engine3d/shadows/DirectionalShadowRenderer.as create mode 100644 src/alternativa/engine3d/shadows/OmniShadowRenderer.as create mode 100644 src/alternativa/engine3d/shadows/OmniShadowRendererDebugMaterial.as create mode 100644 src/alternativa/engine3d/shadows/Shadow.as create mode 100644 src/alternativa/engine3d/shadows/ShadowRenderer.as create mode 100644 src/alternativa/engine3d/shadows/ShadowsSystem.as create mode 100644 src/alternativa/engine3d/shadows/SpotShadowRenderer.as create mode 100644 src/alternativa/engine3d/shadows/StaticShadowRenderer.as create mode 100644 src/alternativa/engine3d/utils/Object3DUtils.as create mode 100644 src/alternativa/osgi/service/clientlog/IClientLog.as create mode 100644 src/alternativa/osgi/service/clientlog/IClientLogChannelListener.as diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9aa9d49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +.idea/* +.idea/**/* +.settings/* +*.iml +*.svn* +*.DS_store* + +# Build and Release Folders +bin-debug/ +bin-release/ +target/ +out/ + +# Project property files +.actionScriptProperties +.flexProperties +.flexLibProperties +.settings/ +.project \ No newline at end of file diff --git a/libs/A3DModelsBase-2.5.1.swc b/libs/A3DModelsBase-2.5.1.swc new file mode 100644 index 0000000000000000000000000000000000000000..b851e7a9483a656877d5d8a6ea0b1b788370e061 GIT binary patch literal 200029 zcmZU)1CS<7)HV3DZQDI<+qS!>ZQHgzZQHhOOxw0?d*}VX-EU)KE9zEd#mTsNvmz?; z zVbB@Hy&n|o#(C)*bsAtOyoHwPz8OEo8Fm&|5a|m74gNo}FGa|NCsr3d3e0LT;i#I-&Ex zub1*%3>g6B?=KRu+GsTrLk0k?=c{_E-q`s0eq-=@QtMHdBRJ{p3bpIIP<#7)zO&`} z`94ownW_Bn3t##9())S&>C*Z3`KWlk^nRPTAgEkgI6ObU>HYCd+~VCnw6i53sJS*< zyC401SH5ALJcZw@H?}goqTB7+iRZ2Ht@_!6U&ch_`E_pjQu?9s{cy{EA6k>@^uUw7 zg_fJ7UAIz;U-ED^_%OI~Qd&AXOx63*^1q|ApPs`(xzM znZ{TB?U2e(OI^A8edo(Hk@^8-CES%QYlpsO=!xm@fIwfH)36k)r)%fu)%f}G{q&4- z@r&Nfi**M7UQhQ&(c00sQ}5&&!aaGfv-0NcK)6SQC`wY#=YzHINe_nGpARyM1jnH7RzET{MVg?)!7_YV#)=IkAQIbZr7b=#fM z{nlv*DJai}u8e864cBgUV|rdwzaNgwqt;48{;QSQ%?|bn@4J=97R0+XmfTwC_8f0- zz3C@9!thH3Ily3J4xTi0~0ZvrzX6 z9)~T;^4Q9HsDGRI-}drjBe}Ms-hJYyQeR@D!lvuLUkf?j{5}<7UAW&=y?2292Lf}3 z;rZ#W8#k`46nSZK@lWa%)O3z}@k<|;I?q0*eIL9GYd7{T8Kdt-M(QU=&m`Oe9vm7p zQ$4c^bj5t&H^8>+6}$$ojxDquP%mmZWa4SAIf&B3RCl`eU7ar6^6D<|BZblCnJltHXw7etFQ zrtC4cWtLn_z9JOyGmCczaLFx#>2SF;S~@j6qsw_*`HugLo2j?^WA3fPgd2f^jmSJQ z)UT%?VVrsiJ@}sAQ~uJ+S^mY5nf&1Q*Ny-qeJVWVU4f8g$!=?K!TO#u*H7<@0^^7O z?Q$$$i<|GS=raF|#{0%_PZLTGzFKa{j$c_tiNAWSC9zn%|Bwiw)+%`z=fQ?$4tUIz zG~Bo0GwjbhlGd6}Oz`)Sy`;dMC&1N%^S5R*qn!lbqVRO}idVNLZ3+8Kr8noKnpSc< zJ;Fn|;4xsnSG5n!7!~rI+9{bcaq_pGOup@Llm**HS4J$t+Kcv6u>&tE@gyez*)V9S zdS~G)I&nPu*IK;lk$r90*Sk~m-#kxMsD;KW6<|n2!Rw4&k@SVVD!GcCsB2i6CLw#v zH|1x;e5>`IkErIqv4!u;x+9zwJ@BicsMdp!r}cjTl!uzb=WwU3e-utywdMelwh=E8cxG;#qpVg1`q~DUCiLYU(e#vSD zN_Jibz))tuQpm!ef3%g6fBpkM+PGVE7flI)mQPCc<=S@#=7Ngv)?=2Y_VDf$VVYR{ znIS@72b(oU!1O~~kMVx;brvDx-d`cPhgNR{`=3vV6MYDUSx-0^hT|avNd<(&!lE5k zU7?tejk8FM%Rm_*z=1J?SPd0U_k5j#qaEt{|ATOGm|D&-;Ot+ss?is6!FdG8wd(8G z#|4(2hqiSF#5bbfOSgQSe(nm}=ft5AM&Jm{IcAQ?_uV4PE6zx)+ggC~ksbOs#scIR zSu5@&WfIfK8JA%GjPDUy+t;DCa2RK~n6G3TT9Te7{6p}{)6TCBLn%9;^w}h;8Ax>v z>*QdGvfLSNWVPCQ01%bK?9Y5-&X^$Y-upZB3PU#ck=y(`hsA|zP_7IrXCO)d4agSd z1e2}$u3RT~TM6dtvXS0A@KlmKNTpDp3eRRhZaj=cU#dSnkRAX@EdoSP<`bX1Ezapu zwX#Wi=nsz&@*U~EBX>u$cpF$a1u3jsSUe-;Sv>&snl)!L})U7 zpMm5>EZwgkyXUkJD$@gn`O04+xa}RPCRQk*%_7)~TW=W^zddn)i74t(gI^*9EvI=! zyzd~rc#%8Ymm8bmL1Ib{L8ro=V7T~FKUF)qG?50&A%#W7KEUeN)1zo}Y!Y{s}`&?d_FbKW{kezKjc0DrHj)jro1uY1vf#G5LW>(84yo zg{ijEv)Va(FeTcpMt{Ti2!*|^0?io01SxF57DI-N{5&wQ9Fm2-O%rt*og~o}_s(k6 zc-;8rto+pp1^hFuca1UpBKrosslSckCkU#1n8?(lCxZf+5rq{%oo5wRvEk@pk2slT zu~P>MwH=o?#%1i8O01chiWIGwEA-?V#}H#Z2nq#-93z%x^Z93?vr>2`*)+n`;4g2S zkRx3=Lm9vmL8f|e;&zpoTVYYfPX8TKiB#f_XslR#%2ZkbS9~cHG!hCGVmQaTgt-q0 zJ+!W}S-A-yiy6mTPCvAA*>ZX7qL{`M$555>5;ssk4GOHn43%kVLktPlXCi(CiMWd% zkWNS_C=lq+z=ta904CfmJH#0e%79h3D&<2o7RF=@DZ!Tn69SOk1JAcR3g)0?0T2rc z(B}2TfdWARBZJ~@Au2QjXr2%iyakv@%jHK&V1)Z z=sdNw#utUL&V#r}PhgqCH@i6NhBFt`M<8}Z6ccJ`39H_kI)caQ0f-VuD zqd4TsNql>hRojHlEEjh4-9o)Loo;lNcjnLX$xp~RbxT?&PnkW@STiO>ld3qmqX?1n zw3HDq|AXAhvB0snqY9)_-PZt{BnMQKAib-}vZV@yYnGy?1V{wNq|7?;`hJb8MW;96 znQ8T|ANSL(RgF5EdZ5y)c7lZSCNg_1g$pjyx;cih=-|=iho*XWv%v`ia8z%&&btrB*hqUS zS8gxf-u(3Zwf$lFow)BA2}L}>FckeZgo9Nnxum-92a!BjBoWlMocItW5f45%hP`og z$mTR8%j$|n94KTxj?^j-9CIVaSsC2mGc^c}cpZtH0W1KwWsC%@Fu#xXUZ=63Nj-cF zt!;a(`Fl9H&oveoJm3#KA<>O}+{>VIeZEFRde0LKMyREZ`_-aw8V)f}2ct?hSAIPX zwpD^7EEyUiDk!5&d2GN|;K=eC2h<-F>{z@%T2lrg;=(o}*r}cbz(9dU0I@bX&-i3~W|hM#v@Zd1M5r*RkOA)EP9Hi< z?oP;G!dHAcZ+V4%Sxz~=n;keK-Mq1vpALVE8nJV3@>OpAv=6ua>=C=Qk3! z`CJ)q?7cBoyCPIrIJ@)nCy6i;BKLdiq%+ehBGq!)iY#4m2MS9j&9N_&Orvd204Yf% zvvV{BB_nB_=h01lvnDHG9rhCE7aD5m!qadLo8d@xwl){a(Mto3*XlFBJeK8a;z?)T zhx`1`r;mhkXJO*~FFw>UlahNFcGYKp^$P?BfP$zp)M2nSQ#A&VYGd360hn^-o)gEN zPl#tnr+a?hAKTVFyL?76tX*4Qn5w97-Tkv&OmmB!#QAl*LuWruGC!S`v*qWysrB&x zeeLw6Zt>-Uud|_P&<7|A$a71_bLa!$Sm+DCUp(qDPiM|}hGLFe*J%1Uemt|Lg)d`e ziJWojAHyP#IWl*EdDfo>Whu2B@=Dd-ETs~k`1h>*W*6{?k#6?^(G*lYL@9s|-V`P;fM<)tbIN-9IPcqIi6k~1X zbwqrEyUXSEvI44A#y4b(n)p^efS{1Gpw}xO+L_$Eg%5&#<~lHp^>*o8Srq!bzx3Iy z9t~Z5f!+ph-~W*N^)@>k=;pGv!GJB6`Z!Qv`O9Epcf2*(s-!Rhd-9V;aEwTa`M@?Y za>G>33c%^1D%?o!0+i_HxI$>)v7;Mbjf8@N!Gi=rSVIK}eOB>DvxtC~O%-riFwG(c zgTbNTVgkU!rUkt2Hk8$;S^xriCosyu`Jk}xpeXp5^e*3V&Z7*2F2bJuGj%6+JQZuP zNxb|-q2NT@?@*b>WXvk(B(PNr=>GCT2~_`R%9EEpTfm!+yqEiIq0q=I@uyDfS#+LB zpZ#pjy4qLnlynxhPyiV)BETOOH@ADR`6VEFSOntOr|>wZd09&|bYELVYML<++)#|Z ze1t0Xy%gW^IK8mDldlH+JW4T94MDn}<5Z&At;w|#1`6dmZr}*Q;siWOQ*!P;fJope zgt0_=PDF1TGjqPE7fYo z@RPt_(;S**EmcHF2|(UOjk)iLgU`D0i|W<(_{CaliBg8bd3G!!^Z-oIN=h@s%fR`R zW>!N+)%Xv-P*^6nB-~j(rXe&YqzDkfF~1W!M@_G|aEhiG0o)(Rn9MArdS#>T$4jbN zw#O;EKWteg%QO;t6kP(^<4~aUrPW(Gj!oK$mBBw;e!guIWXoJjrR%y+-lzvCXN|nB zE}QoG&e`&?Yp%B~wBsC6&NrA}u2a7`)>)xIAMH;2Pwfa&k*xii-~aN3!HVpu+g9z1}ul zRTMNClz8C?gN-4fca11 zS~vTOs%2*PX#QkXr{Mf(+>zJR*sVxY*`qTl4MgBzm#C`Mhqg z?@p#ccAMvCx47iD3>u%TS?ecj2W8&gj+SNxA&-X`koyA4{7K6cc<5kK$* zN;SG-^A~6w;Eg=Iw`EVt9TY+vy(@ojFpXNtd!a1A0k6uKH` zg`H5lADt@EI(H`UzAA*{RTQz3U)F?s{?Z@>3r{ggyNMdHax1Y9AvI(Uso`X9%T{@y z;c#tCN8#=UbnZCTy<)~5wC%~0>?-&mGKB>K~c%?+8$9pk$G*tg9|LW zINk)Tk;S6oHda?4y3yO}Wix{ZJRb?mc-nG%<}quQeihVkAl|B=Mb||-#|ax4XG_lr ztLT0(2*1q15HPN}P4Uvwf9-Ab%=K%f`R$X;XE6SG4b48RT zujCRZXliAW1kczPMnGWD;)pE;MyAoR-6v0Z)CyV${t^tSeFq!^Z3!)gz1s0 zG}~6#IU1?=SfQ#qxl)SU@k9UyQyuo{`MZIU!5og6+-`Pa6biq19DXQNJGEz)T3LV6 z?_7|Ra~#9BrK?t~H-ii_J`>CRXgj@C4GG#>l+>tp^@HsesE(|8rL*z@$-%gZ@4KkU zp$2Pe#P99_HwtYc_yz9j?N8B_YnmgVi1Fkx&~~J?EyUk$iM6VnJm10*mETg^o|k6Y z@B2B6gf_aCtnVO&#=O?sHw*JEx77Mz8ps+u?uJ^E-x|hQzlmTE!53C^_Z`&{2KNObT8K;+85O! zKYJrR_M{3BZ=TM6*+`^SM0|2rJqdU5uW)kTu1yqH_uK*E>Hk|ZUlSX6mJxkeW&CVB zS)GCxcF7^8q#nl+_ZN>?R0kA@w`hyhWFg^x_8Bn-T!}U=vMRZDhk7o=oTOGlfiPHGhb?oX}TAaubg}>Ne zx>MV$y?up$tC(bPu$ME*?soOZ6`1Zf$0*=lE^fd&H?$1v(8x6Q5iYo~hv7oSP=Mn9VXDR=Qmfs)p)aM< z=b9I~2cgo-@DvLP18Fuv)nunz=?6*-hw=vs2ZNa779-=3uxAkB@c(nn!1^QEhz84m z%7-D~4~*TWMV+R)^5^>vZ#?y}Hm2wM)bT#ImLDHu3tc1n8gWlL-%BaNAu6wu{s~i6 z;0ODJ@qM-d%R73jcdQ=Kk7LIS`PcmOh)S2cJ*c zR|(e0_)VBnfcw(Z7@Y&@;SloG$ka2ZVh;AUsjs}JEY6z2+~=KsiP}&Pngnmx0U474FpQX)l7Lg0V^t^ zu#}HBqV1kN6gjdCX&NT7Oj!U-7M43u09F93gs|O^e|^>eKpe>kf*AQ*L*cBPWlkgt z0~})N;r6zmTaID?#8HeBTcQ4PwiyfVp)eD}PBw$4**qeWaH7QkA%D_-8giECpa_ZC zxV$}QBzf5%8D`P)00`7`5CX6gU;)80@k3>`6cw+g$!3@Cy#+4!7^K<5+@&-0{G!V@ z4^S!(Tg?VG&oWf({RB+IQc9{_Fe7~BVoV6Dbd|^=r7XGPRdITkQdoA7%J}|ue}BaY zP-iC}E;EKsZJg~dFywhqQS8oEBxym(fIo_Pf=3ikOy+QiNOG8thZNZpK&0mX1!e%4 z&2vlw#cCk*M+B9Y*(4^)vaS_Zp{m>uN5*hwYz60Dgsx<(u@-w&W26558e{9r9tav>`!y( zF&`T9;S)2zL-!0fQKiv7fJzLZXk?(ZK~!q9~CgN4~v0lck>XYJl!VVF0Kg>MY@sKh*<5JOGPsgImI~#jBI*#;c0u z28vT7_Qk0ladno5iq|~zJU0IVux~;RpTH;j1^X^gn7av;|44~S1zOzE$@f#;c$RD zz0Cce);y_%B%-GjCgG1r)E4V9-x7_aJ(2JH<+wxlX~+jpskkwAC)t}3P8`m+o&`(? zX3!unpt-@6@c8RYe%?>}OpVqt;HiRZFPs6mUF!=*`ivkLL+_5+uzr*gNoDhBw7k+Fo>6?_>ty`|>Q^ zIjrmdr*Nd>eC8Zdaf&S`mrNxc!RhmE&w9Ex=Y*XfbPQSPT7wTJAPiRsrsa&fn z3qo6mYbK#GMLP5;v1SZM`%`g*9D8jC3o2p7Mnh-pyQ=ThiK7zaV>+~o;mVaUz@Hch zg#<%`h=a-Ma0@d8^dX{z3#oQ3(vas1{RcQOkMf;N5Lg=5&UT{KAp#f(>-ytYubS3m zPA={v*5|vu-fL%~J}z6hO?V9r;!ahJ2B$u%JNt-6L+=R!4P*jP`7G>1rG+|kkaSUe z#+n#)QH2TXz`cLJ4i$KRIdQQ+Df)+Y8I4%nxs3snhpD6@VZhQ{ha1wK1W=lV0oi8q zH~FUkMfrx!t>SwZ^nNnNNdU&;1SfU6R>U24xr5=eHqALa}Hh%>Me?s!PUHz;@gTNqA zh*5tK5WBjEpS1nta=x`c$}vM7C$?>WhmNJhqQG=Dh7Uplx7*u{gn_At1xyeqnNf|! z0Mg-@u_c(%iJUT#f=aM4KoD$^0punD=O8m2rE$!NO!4LI33G=3paesEOVU|VE)-tB zzx6cV!um9%n9N>jafC$uP+j4j&wvUpg+70xCcv%`nSRN)nokC5v@)86EOg+d3bL15 zz1o@J2rQDQHiHBv5|DJhs%rcYUxW{`7e@Wgv5vL{cI-bE1UrJLPetY*`kHG=753u` zI@2+Iybxwh>)08S-Rf6CB(HsQFoftho?axk#r480N5w40xBbNVlij;XAIS^lC=_JI z@%w&PTGIM{T;#?u@PbPN(n-I}H(%{3v%BMI<}@ypCD^q5gZuVr8Tbw;iHJmuNv^-CiR}gqx&4z)6ynAeFv>YRk_#S7+-v`8bE4z%>?NQQ!}g&$ z$h>-92}K}(jE@+T5m?5$W>|4!ibLC^b1mRx0s`bjM?LDDO~6p+Z(?JdY#)4-#O8`A zKk^G2WPg_H2d+b1>|$jSJGkd~Cc`*nsbHob9(sHcpQKO1yL-xE%S8J8>CEmnaGH`> z`=zo@HRlUrIm-jb%}l*UWGR{Zdr9!+ODII%r^9|J4@f_KZ>6h*vIv`kxao*a}HfCmzeC>bD_zv(eZ1%iN(jXIyF z0I|{@GNpxak^L(gDHL-*v~uJR^Qs)LGsoEsRLL75WDrLuKaYMO!?3rN(-+p$;+ z{N=`=N#suh$N?zG(hZG2(UhjrbO5DMK0jvk;9FK7VL-orzrfQ5q%b0MP!h5jaEDD& zGAVBmhUT(>#*q5EMr^N zcLIGFi_6rjcl=;YN&R1D8ESxO)N__kISBmCpkr0xYOP8({%~Md0u5Q1BH;9(x=wo@ zl0^yzy|tbbL`5k~eSNUWCL;723XKYA7|!xn3L+B#(Utn~1Rm``zW`ZlGI2H5FDq3PqWa{38go$8s)%F zP32(07{k&*#3^W8RglHN^1&1)npc37jF4{bmE;V98C3(3=>$A1s1n^2 zQQ1UhYF>u8RH9bacW8K7RlI+et$djjV2jC|+x;*hL@rEK1@O)=1e>>bJeVxhCrB?(QU6v+;HE z^CFR)_Hgw|Jqg}H_3MAZ-8?bRmh~3@Zu@T$_1lz9m2bQ}LeR-p>bb6tdFo);zev;d zmyJWT@Oby3;r}pwYtTMINUVLka=P2QHWg;Gb7Q`DMN9NV8wVVV|A}WrW=cBO0}hA= zq9JS`HP&=F-@ZftMY*ZFA>c#I#x|=FCp9fv9|RtF4zT93M{e9uUrPrM~eM3cQ z9$Apr8V3adi;*D|m!aI@vI-Vcta)9cKT^1yFyGy$Y=t9C@x$E~seFv#l9CaRf2AL# zs1jSu%$h0&egVl8Tom`0wil{JUHttiy07Oh+OrGSi&E@wxlz$+F^-(gxI$3T>whW@ zk+O|V)yg;{BB*_Ex_F&NB3U_oAJERPFuPx&q%}hu*M3#^0ueY7g+OK3zR*vm2*Kg^ zLSXhyUbw9O4x#zK;0P@MLBSA9K0@*NDP*LeN!JT$w+z#>51nH^{vMJ2F4T)R?HpGpw05vx)j+5MgA2h zvMD>n`b7~J@k|m(*w+q8wQoE3Ejg4J{tL-1JoK}!M#c&{Z0J1(HjRj=J_#2>2C2Hn z2=spaD(PGQx2b0|c9)&c5$X3*&*ezZKcfLix}SD0rQ(#teArPgBP=7>Z0Hmfr`?N%^IWe);>Uj_ zHz|p3^p&5U^?y4OkL45Z=Z=RqCru-580?ASjD!w57-SguO?~i404lxSQ8AXF7|>;m z6c)YG>gz@kK8V@nZ9?9g%g_J^0 zAuEpq7FF%tDzDa0?u6u!^hLFORenbPI}+9Ulo9Pv#y$Y;U5x5z@t6E+n(uLI-Da{v`FDi>=YzQOYr%=s>DJh9XJ+AGyk;q1 z98u_IiSxzi5`TXFZH+hXcNADAhC;RPj#=J3n8!cOI;y3GV}0j-m}+;8ZYvUK1*mgfWf15iPG8Gk(4!8x> zvdXxLZP&rcXl4{R8U>$;%`6@a*1XNYm@p{(P@M^qq`xmMeGe1_3Vc5#B29g7KAI)D z$MF4>LB``w{Op{_l1;@@LqZ$zRCCp(5aFh7?(mYK&*dX*gi4PN)cjPcM!tDi1v#=F z(U!H|nuxFiKki?mzORh1Zyj^iJv1I41Th2^N)Felzxq!A<3~s$V_bBAvL!GTw7x9^ zIHm}sO`tT1Iq;kyp#`ucN}D<4>N8nQxTX$@4?LmbB6g{HxSE@RyCiY)V1lBkIk&`k_gZlc2x~sHuge z6xbFuq>+-S;v|utIL)?*8IcE&-96&~BShid&cC7{Twv-DpkZV9NZ zh|^wytxDp>L=%7Yy_lxSh@&4V_o^~`zW6BkJ~_2$OSULKrr?1`ZMJ7+!>N+3s&b3; zNFcct7_Qx_tX7Q%t5W8BJ%%_O2HZESq*CpD_+6=%0YJ-hR7AEP4&eF48`~EDhS`v; zFFD3sS;5Q0g{BKApD(LKjb$$2k#tBfgrQuHvMZJ{X%zKV4$aTsfgTMc?jvlF4!N|J zC6Kb=Ee+q?v2H@B~vjLRbu6b=lPn9cM2 z0%b5RRiY{;1nq49+RW=eg^~Fui2Q>Q@#j!;ZEI{~qQ96q(2-Mp-~&ZVV>_$rBWy5n zL91PBFT4aBmsj~1 zU}FTR*E#?Z0s#|4cg1j=Q}tT~t|W%{j1M9T>zGrJktPXa2|tSC2F8o9$24DMjWYId#mHmo__nWdlH(@hYyA-#2X6(4@f*~33v z>Z0~S2H4pG03zE_`-z~Nn~dJ_{k*$b00EPFl-kHAyc9LP>5a;@N7Hy(-SHi_UfGAK zotW73lv|+ge)~IvO~7&^fu8YQG!R zaqywALh5cjf9Y(r*!;ie^JlJynQLncm1J&K(LK@cv-4|QB381~O%zCgTi*pfoY!TP z%KRd%i=H7%(bYFDBJ#RW9HC`4HebY9?YxVnz!_-WqELcnd<%Y=*1H?#m+rhw>iHw&v7mYV)$>opd2!} zI=GQP_^M?8ZD#LSnps5}gL#VULgGkTwwIL%%IgyI>UrIhSH{B^XI1Xr9yc35m>wO! zAC0zH8Yh3yA!Pkz+tU?-zhMy;tIL_AC0%`d<}Ffn5LR`~8A=*ZO|E%s0tXhKndTdN zrV+ZmBY)2 za|_lB#o^C$YU&u346-5BI}euXy&Ippm|mKGthY!Kugq@cS5|YLjXlm3@M}?z>Y;z% z?9*B6Gg;5)<3MI$MQdScFV!08iBAT+9lqY22Oe+Qh89 zJG+$D$L{J(hjMk4fTMam$MndT(u>PO(8IYE%5nIfzafu{-MN~6IX<#<>5g_T?2nuE z@JGZrdqC*8vo(Kt@~cIf3TfN-OcIcK6O^^ls&Z~!ql1egV2JS}X!zpZG0#_-huy({ zLv-1mm)f(g+_X18wlp<2HPg(qHnWhQqKA!8r&kDRs`Sb*5r>p4$8Y^~O!5>RbMU#! zc$ryslDO90WjX0^)pnNXh|9WR89OvbwQ{_lb8~q%c6%~EA5wb~>*@+!cl9IOP+wb7uL48p>*|i-#sU1+pdgL zd1=+TP28?-%VKUV)EoN}j^iC$(YA1aj|#$vFn3WC&Rp@kWyU}c+IE@F?Y#xFN9fis zBJ_0Ojg$w1XM0GM&)zeKtxr=kHpFFl^c*>?M;%pl-GLv6QT3+DkdL8mPrh#Vq4}0_ z#ZgyRQ+M8D0zuC3nm$)a3HrLfp8tA-wZ9UzY3onEP z_)RS>gaXkR!VTXLTyZ#@zac0glJv1pU1d(goPQ$m3)0=0$<%LE6C>hI(zs&|B4YB1 z4}bD`=)+8)w-3dZp2t5ppVE(_R>^JVLjCVx(Fjw@%RPVYCsr3%&MPuL-zExiSA~-; z;t$Z05OVTX4Dl8V(8ps2;pyTDMmHJ--NNHGAh!uGg|wCxf61dgx3Cf!_KS_Vz-J+`g=T_cB`WaBW`6V9%idnY;Y z&1qixl9=;HK#KJAk?jU-Hb}X~>*O^A+XSf(vF6h29p(x}^Wa#;Ls0D;g%_it^ zJo!}|BpO(5ahF=2=OT20+64~j zPDOAV!!C}dsaS>TWM-F;)pOS&HV*s@S64gQO?{yRm(bbS$fEsiYf6>Y3}4oz;Hm`i zq2$Sgx2n|p+*yOOYAP6Rj&Lq-A#QjeP9EpQ;)*ezR>OH-I58#OFr>3gtkS8Z!ruoVFxq5v8 zHOctEBMbY1@X&4Pq3I=b1v1&+l3Po2zc+}hwiSLD3T|*Ko_S{b51R0swyalYfS6Y9 zdvtBf3r=zds4BHIygtciT&Pi(?kvi~t{B+yFz%*rPv;bsb4O0~{cI!OJ1rj>>}(ml z*38*Q8~7)_LQAZyEM#YY2k~-yyg0KPTgfI3e>CL6deW+`J7{EX zxYxV-2@R_+vZM{BJg*O=k34D)9AP}(V)<>OBMTT=^J1v%3+@j>`zhX4fW!Dy;oa=2 z5dckOx+}a!d3>_g0Xc{6${lDej76>2Pr``|dBGwzP0d)>n{wM3?5h0EKIbX!mBqSJ zs&bA5Lv=+GjjF~Ou1GP}jQ>!23TXHGb*{VE=@(SR7_oTAai@%b&E9?S3jr>CcTgb6 zWa?=69gmhm;O7sMV20waPa}WLW1$2jVpSn?@&R0BXJ@f`vlCE50caG030s9{eDJh_ zK1n!w&I}!Oh2tbMa)cU_$hU$WflvdeWzcZk1KR~vs?`jO1BXGvRG3atWV2#7&||kn zev$Cn5)AonXgYDTLpK@VS8p&vtDy5ZqF$sfQ3@68m4CO1>ev@(KA7mzpmPP17o;xM zi|1u~Gi(}ac}o`g-s|tymfTV?TQ}0M9~?(|2*#5m0?N}u@fZXTCC?k_v`dA!T4M+c zVk!8zSM2%x{GWK=z$`A&Ewj%6u8<6>QpJs3-gk4cDc7px zmV_A}&1xo9!(qR_A6lTZ#w>*lN2#>8UMe0VZq)D6NpJXSDR*kR54KS%n*m8Jwsc(5!(hZSbXuFg? z1T5M6itWD4>&b-=l8RQ)2tnH5$B+Y?$KVA->v9oH*AiHzv{3H~N$i70N~%<+NlR5R zg1Vt@8G$DZBee01sCH9ixXm>5S^mtlsjScm?E^U-5+m2Pc1Kt70E5trT8L-m0X;nBh>RL>ER`KI*K@;leP&%op8`#jVXI4}Ea@^CbGl+mh z*c{OH>lxT^?&rSFziZjmjN8B0-@3AMYFYfNi82ykceGGxv|YprtK}~y{;;+m?=msm z?{>72=TuWWI3>PP31b95rMZ6$TE=Ao%ooqSwdbD6d%b7Qnfc^=zcuHVePd>_Ls-Gu zoe<*+UCIdcjw0y78^9mqFVER*!NPKxjY%?@iL=08MXTA2`qH9~O8it?Y z6E7mQi&;WX&F9-Pqbj8{tx=r$(l)`+%Sf6*+T_Ul1RAdVg6Q3JTZ~^~SS{&yE!j86 z7}kTKRb17GrQ8j;Y<{QWWSyw;rNaOaw}mu=NU_^uiFNlej@aP(()dvFM&VYBF!8RZ zF!S#XCk?Y6*LIxz)h8)Jhy*N4;Yz@<`?rcVvgUzTZT`~v%OBCgBW8{bI_{-5r56!x z&B0X>8G^^40C44H@-7F5-t36no3y1hxr#(io*h(@emC}WA{1nErPX3X4 z=85W`s1Xps0m}fK4bti~Y4grY6R6Xki*32MG48lS({xQ=z&2n*|3gr?)q$4vaK<|^ z_;9JLPwlmagZ(0Q@cS!@Ogswfhw_muLM#h!Rr16bnp3LEB>CJRGM-d;ef?Z|F_BNu ziacm!P5kII7G_JJAYVxH=2I9eum2_S61W`sOSsJ3M3@xw>S0gZ zIw(@{Mi!OfG|47qKme^nDw_8vj_xx;1W-g7KH}Odr)e7FA`!1A>Tv`-Qv}~iZz!)K zlPek+eq3gWL_T$;`){bv${6!EssUjYGud$3h3Typq{Zw=4RqR{l+^*A_EE)S93W5K zFbQ?vtZxJ>TnSDm?Q(^Q++`glf>|^wlTdo&(3_&$Z!{(OH@qjZMC{7x;pQE!HTZYZ zC*76^Vz}Z~T2drQCX|*x`A_jI!S%)|hH+6e8kd?&!d7dYeJg-Lk5F(OziqP1>Gu%J zy})$Ipnh4rY|JHT^xPuI7g?@UycnD>%cDyl9CAA zh0E02vJR?@f!y)e(~ekF2qUtRa@;9M92KATHSKh-X4Fuhm>U=IhsLa_A3Q$15iylAdIbk9{BsWU!NEf+dILu6NkztP)ogGmNF-aB zrALm=L{GdUtP%am+>AV$9Pu^Q(taKradi2I%bIg7wud15nSk4xGM}N$q6~(L4X?b1zUY}quU=h zUvg|usI9&x{`fN4_D|#(`z1WBl=qkobsagZV1vAcL$5UrqZh zO5y4`US=ioq#EC$q`9?K^8?jcS}anKV@x~A(LRJYS?;0UMFmTEih z3uG8=PhtL7RC;3(m$@SdZXbALK6X2;vMi3(5(E`i-8|HeK3`;HP~<%oqKPnS#s!}< zUSIckrXQ?$*tjx=ZHRaCW+NdvHWUFn_v|sQMwlcB&I8Zx%~EUw7X{Sz`|j7bbkcE3$&PJiwihd%FsmC}!Xf;XiW6L10QC$*>Q(7RpxsQ(P&kn!QsXA8B}Vab^>|) z2FI1wM}r8^&z$2)%is2Uu<{48;7z{ACHoEHu?<&BM~skz*MP=mOk)5UXxR~h+G+)0 z2oMt06L3mwmZ^2!M&1F0z!h1*Bci1LnMcm!2Q-Jq%H2ju7b*4dO;yeMe)p>{kEptO zE&Kw2klc=jm!-uGIL|%q8=P4VtQiJAzb4T!8|?kyJlqn`kb3&qh`<`|-Lw&Uy@<#d zL(Yl83UyRB5*ail$0IJBv>RSl^{C9vk6=ES;ZN+{7hAN`xx7GCZORJhK{ZUSu`Rx@ zT5sAp-{CTZY1hIK@{iSG^B$V9m^$(muxMuxg#?SS|DExCaR; z3Ho>K^ACGIfb9E_l0bZfC79i$2+O~UxU8ErKpR-To!3u3j`m*80n(5z-JQQBUpxJy zaYB5ZX$;f5R?G7CeH7Xd)%||~IY7q0@B1LuWMp-8_RW=N*i~>R*=jDQ*)*9Jb=)n` zWhc%b*)@DF`dPNtqOdrI(G9U9m~38QeIIqqv2bK{J11ESHduCdsx6#|8CGDpSgj!; zAv@TQ#VN>)P;bMw`9NI&jinS|#Wo;H*@cphz(2S&(IQ{1uAVF>R4jMtyW{PHP`{bC zVCg_#h9VgwVerx|V}SGQtW2We=^02{+tAD>5IuXRH14KNYOsXOB0wLmvQTW^6Rm>X zF`awflb}VJ?5Y3^{<2$H_Ed?UG&nQ92wc|M7f7!t$(WpA&-h^^t_UR z{MPWcNpOaJicvQTX=w<=3 zGjQn)MI!itRwB3n!w`hT9DSeJWQt@@acOE+*4Vezv>T^i2qBG)t+Y_kR8&?Wf$oj1 z9RXKk!nu+E%Dyj0A5Yyzlsvxz&`zy2aYrK2b81(6LC>CM?!wC311Rl9=eivv|Ld~g zhxOf0s~S2_C~pO`SxgnX9#*t-Om-kH#?uBBhrKZ9u!&y-Ph}n7oS-QV<`VCx_%z$eD5YWp*XN;otEx=yRxjnoQUpJiz;>kX%7tVmclvLkl4dN7+7q5(P|o z&+^$U9@$NgOcG?=0}AYFm+WVZc;1{_5wTpiAJKbj4RdCAVNWA*PCirL(>QMk*(d6J zW>+EtYc7LC4zmeI+L%2F=gwn%5N4A2XVRxyakH+Cz;Rr;t6MMYM;(5|vmVuCo9;17 zm~zfIvu5d6$y7~$CvbkN@}Huxdk|9xoLj2hoAosHf1LwZ-lgx%1_M^#q81D9nEDGX z(qZp4Wbjmhc#z*O8Enxg)qn$$Ywe=clN&}nY*|*G{PrS8Pt^UP5e(HJDTMU`5S4iN;9BxrF8&blZCZV`yoF~bP4Y>5Kwx{QD+#$dde3kubx zh3b+Cd!#ZS)E6+XrlLl&(kcC#F{OpG?$o)V{ju&iU^d(V=6&vjIgX=ZhZ5n;in@ss z)>5YbM8yac*-f}X|75vy1`#tB%9#*F^pF;m2aRHBfdG;qe*?AJ=frWUtU-&OV%51X zO{v&%I@bxQ8j%A$?H~Rjk3K*Cn}yfK%Xl()mgmc`)+}&_EYV{39Ite749;UDN5L4O zZHz)SFkT~vxP5{I9kt{C7bstWI_Wm}(Ixodb$K_5Ydr8-0)QhIz)vraUTt^2?%26K zSag}%igKQIJlbDU14vknpve@i94uxLw^&qxQjtEVnGp5vJa6OD6z>Z zxuvLHDP>eaE95n1e#b(+qBo}a153@aJ*+rHv*-o>ODuxruI6#~(*dvUJN`(Nir@4d zd{RcuXY>(nl6rDReWu|%;22;MdqOxxlIoEPni{9UYtS+MD3z+$pl$Gxd_p}-;c>aWw%QGFu-;N3) z{c(L@NT7@!yas!KjNl6|%pX$zAh`U=&5pN3L@5{N-loT^z`XZ*iyU^kH_MU7-qPO?6>bJgagTdcxjedaCat$E}+8y8(kiV@w; zCYy$@W4#k=X_Kx%01qcK$84xY=HkVFx!n1_yfbgs2YEph1r4bU3i?T_f{uadk81{DZ-P?Z5$f?j`+$^1QSKO7^Uxl!xmFyvPEDJmRwF z-@CS)=l^@`8lYI=UHXgL^TV~t{H(L7bN(gs(^e3dF2?o@T>2}d^G7iKj2nI2HDAZ@ z!spNHtqAnlQu<5Q`AfE0Hjln(HV>cq`7|%~oD2TxQu7g}9wvswYw@2%ob@d0iig=C1y7mXUimy2Kr{n=Hv|wvG!@=y<%I4Kz&mb9S zszJN|`J$0}mMhsrZJLjeb%?*ISU!`H>foe0D%rD2ra2AlQCf1|uh-iKbGy1!(B&K^}?`(kL zR!dIP=WYzYOS8kvJ7K`SRdbX(^Qyft(!2QZ^{V>w`&4KK7s>~Hy{F6Qs;{Xb$G+;a zRWZ7X-5Nf*-g=TpfZT@7nHn5M3k)(q#C&7nQ)a26vB zTpFhqC~S=$u8zT>9i6>9)xn+S=)MyYfHpGoZQ>=^4VdMZu=~ofv-SX(970uJ9pYB- zKR0rKunvcD1!x=(XbL&%i8=b|jBy1XUcYaSFkVy(63eRCL{On%bx0~W-?nd#{k$c$ zZ=P1WSK=b1w=noWithPKa{rt}=MO0z1R_>9v=>w?EI;n}myjK6?d1ps1Du@`$cHW< zUydH6sJTu{Qe;pQs-2(*`2sNd)8!cGC~sQ!n6NN?dZi%;BHuZt!_$T=uy!T0YR!%C zCz!_;;raeX2wtr@ZtJn*A>C31c@VG047@k^lDIY&n~{EQ)I*U<_BzO7i#r1Th*R zX@-90v1dE%?*BzGZ?@`Cb=N!YxV|t|Z9}Anbr36aheejYQdI8@)w-GVJS>T>#R$x4 zV9ImF5Q)e!0KY+WL4{|BMCy{m{`1I~?ku9?69r4K(S|G;1f2oI9L$F7)Sz)f)UbA- z+i3T`#{RcQ3dbeGMHcTw^JpB*|N2)z^}raF4~f{py)o}Sh$nUurj1`~{NK$fhhm|s zmR2$VT*XAh%Cm4$c~Bh5)>2*6$ZLf6KsABDzbLZ}B0GTL2M}o^O%A>#$*C^Y9HB8G zbtl9I{CYPt^>w0VSO`97^(Qyix?B_<2SgO^u=^ALkW#;c)CtW*c(PH^H#8LB%|M^` ztIOYDjVjP)BR@AWZbF(0dr1RG995TxG#nHvgZ`V+ig+$rTR(5B>}KY8WS#J&zG?94 zf5e|~QSX_2;7xI)-Zg#upM+8K8GbNK$*KEJJ~E}osheu}&N}9vpi`s&|ChG)cuR8Y z9v`txPbLQDVTZsp=oNvnSz${Jf@ESvS*@{ThQL@Ov27vPG4?~}abfIn%k72w2LRZD zZ}t7L2jSU3aA53(&f&nCRiOVr*q!w9Ak;qy&=z86z#lkk|Nq4Rc7o8`2=?^-umxNg z`%dOz*g$Y#?F9S!=h^++*68-0P1_=+%5Lo?>`NBFtxF7}>q!0XJZGOIAyUXIrDASjdQzG`zpGPFLffPMI6o&TxQTuSuH0T2`&kRx8 z$As;5S9@!C{ChP#!M!Jh=|Q~eP?6do_!pn}7WhJX=6U>kj)=wu@QnV1=mUYZefcN( zcME6*b{!CSLVpqwQTtRne^fT0pOvpDpQ%y%X7gf_J_%9#9kqYjUs#{IH`wQ4Iq3pPzeU zoH$_rTl0%};W9?~izGYFdaE&XW7SUXS~AfX!Biuu!dRa9csU74fn}{T&86j@%~E?An2Bq*MbI)cOG=-MrDLns0#~n`0CAZ$A~3-7HeP$H z%Q9_=;TSQ03A-Bg=S}kI9s;;^zvWc@nXoqr){XNR=8ZHiWXEf|H{%3J9i%q1rp_+Y#RO`VlLDt=ZP1glbIG z-mFWRAfBN!I}k5Xp`();w2`%Sge}VmaYY4Lb*#(LIaK3 z2fR=5u7Tbk4Df%|upbtSBE}$-KH`c>V9mRZ!-)2a9&V+;(~JDXr^9^+-!ZFm*^uZ& zJqf+ye^VEn@}w}vS9-InQ(7;3R%zr8u@k*y4xH)T&YZ^3qTx;7m5jwA(b?@)8>Qp- z*2s>Bq4v+szRcS3!;uJ?wxW+1C5nui_2fMifwPi;M3Q&4gzyGrv*qAqJb;^=iDUWi z7SUPwPh4Xk>0^Fde5dX+xwB*0;=_kZ5CwXA>^XHF`U&Qsr|2CMumUrCUhZ4_%eop` zoU7=(zKe9-1Lr2sgZkh_O?{VZxnR9)9FL^FH%Ei*+ec3FTo&f<3i=?pelK>u->_e& zV_|WBGjZ@~-BG*ecDy_pocL>ixEJO$A+>D=S>0~(2Y3V7#E=2sTs|3-*-zimymRWn1%P;s;@G4lTG$^HTJ{=FX| z!y*LdMjNf2LJ)F5aj#IKh3LVA(_LGv~3n9 zGEb887bicfpJGj6Q`!a9@ zr@!unf$)Gp_EYHxf#90(uDnw-Uai=81JKQrt&!>Um-SK;(^DnoFN!&sAoZ<|nF9vM z{za3=vLE)yV66**0YJq$lfQ?%an@RzsO6F1T%3|m73j6z_g+a|VU_JVt%14pl3umZqH*DA3jxavya@?#r~m>1D_ z5kLoSlSA?xYzt%iMKr$>ErhbTv1kvD0+OZ~HIQn+raY3~usNe~lbletrp&IUc5!wy zOKK$X)*7S1;`-#6h%HJ_x0(`Eo=6*_1UPV8meyZlhBX~aR~q{7iq96`LdA*GjbnUO zc~Mv9UA{B0Csnp{*ioA)qWbtHaU@mWrZtjk(1`>0C)<;XB*;R|5(`@pBVD9A!dskZ zE95X(G((LNt6`23Z-hxA+~Z`km?d&vd^=bq(Nj6dW(`YApEXPT*Xm+eA<19uH;ex& z9T|*v08M+erVQw;uxu(B-^y0xIZ&|EcKK1HO7AF&*4;M}xWyW*(N*|Z+{9^*HyHd} zQve~j?b5`%eG$GUne)W{h^b^-e#)8iRGxpxU^Qkq+8X0oux65jsvndBtJ5ZhRw>PS zZ&32ag_$XM!P5_sxpVbDhKq3|fyab(v~dAP$VyLZAwi)W4!TNqIYDY!Sz(c;%#@4_ zt3IGc*20S!Gi6QGV;j0>AHk-wj+)&9)5uQjiB69?)5eS(& ztIyZwwlnI~5sR47AjcJW#<-PaL(MEIG2d*o9;yE6!6GYrHKGn@sxBbSri)lVvMbyl z3dB?fyKZPA_J~u@pdXd9oOZd|(F;3Q(d})h>PqBWxh+h@gVHm;z~K^Y8dbsR#f(-3 z6yxx|AyKgw#j)n{&aAVuZOu_cYsxORC*A0BrK4ec7>|d8z0rAb6>S=iH5y_eV59jfGJ;tpU!UWrLdI{s*>#$}9n zeGbE4cMJ5sqTnO`j~Ub1nZR8@L`N7G=vvTRIq^XQgv1*lZFaxisXU+Yib6cSdEFMvK`cfR6kwJ~hh!#W~(20P5a#$Oe53Grk zJ5TQb8hX5ij1AGpGyHrhD}uKti9V7mJeWsC3Q=?eG#OGh%v^02 zHvEv3^uF{DR;MUZShYAswpB;1JW3H04~PI44dNq)!OD2l7Hv#ws{^CGum{ulMZce0 ze_Y)^BuO2skqq$){Cn4Gik)G0=7ZGR_IuOjc|)9sBNA^*)>nptas@#vZQ_BQ9wEDL zaHsG)Wr?hTR!Gsg9)HB+&r@((0*-aba2h;67~*8q89 zO47%1I7!l1bo)yIsVp=26HStIfqxVZed25i=YctbWRX3P0kczlOD>9uS#J+jHWRsO z-R!u)AhPXYmLN?=eu5}1BtK}M`Ico0cY67Q7iUAUtAWH#sg1$rY7hyX;>^!(l=|a~ z;l!GlC&ZdymJ9>vQT+c?V__U}j6PvWUDMdr_eeUSNj;(_QNS)uSTR5-ems z+v06yG**!3+ZrncCZTv&534PWHIsf}7HcS~K5fp|Tw9cQ+!mO)sf^UUqUI4o~@WO6q$5OX&nX=CyOSBD8mCmouvF?u7frwd`tr z>ya@_liT4bNNQdANM>_jQ9VE+FxBD;?`lSg%)1*T$wb~ySUS2ppMO6GFGUS(ekyp` z^nv=Z-zYT`Pu)s~Clp&({=Qw#x-;x9tyEqS?j97!4o)p%`CE?s4mm0jK-ckc{+1$6 z-ug7Nx4X(X6IZ}yQr^+zyk0LYU*uHnDy>6q2%9QD<2#OKjAwE#-|=zL-7c3j%e(+r zys+jDTU0oF=0MU{__VnXLZo@XRV?aFUL104l)-Vz8!IB8^n!H;;<2PsL}zgs0Yz=c zhe-R4=`1bteo@jhIfkF!In4$DN_E@L9=y@6=QZlB$z=R-8KGV4GGY9W2ugnVE0=^N z9WJPKIZ~W}qpxa_C=`l^Gzd$8A5_5^V(?s@W0T4I9_^RkNWo22BC^gou4GE}+&oSx zzXQu4MJdh1avjE3FLcN4;42Ho=0Yo}Ye6yxu?L26yDcs#6;w{vX$4i%3Aw{7;Z*H; z;cq=>NV4aGy5zqvxZA6=?bnF(P&rD*gpwhhZf!o95ls$b#F8VJM3W;Z&vuc{!XF`o zGs-y~uUkSt;Z@j%1m-m89KrS2icvh*wFi5;GyFOe(ywblI z``R&ecOmL{^)xV{{OS|n95Zk@0mVe&BG^3k1gK%msxmO0|NBtYs?d}QPs0p*e1;?kdLr`yu4?g*4PrFd6n0+ zpPh8!7wZA7$zbPab&Rqsvqn_5Uk5iuL7&Av=f*SV=410w`_G$)d+)`M?$wsAmdB^F zwNoMRrv*L_9M&z(L0qEVX@S73KgL?N70Zk+L$xLE2VvW*>#|l2BOLii z9g1m@i#&R_kCZT_;F7LO^X|?{B%fylqWPHgtuS3&<^fgp?t%x~G1=U~jq+RASNhsL zT340t%N>l*0~sBQFEjBuII&DXPF@@+(PTPf#2jhF5=q&fG*`AQ#V?iekUJP7mRZW*ja-w}#&U7X|b*nX?`M??pB^a6%JZZ~*21M<-Ffg@ZEBM}PG{ z8F`?VPywRyWCHPgrwQoblqsmq&By`V8ppH}#I&V@B;QO0G^}PIBW|~VtR(J41xH;% z_`-Mu#(Lfp;m$zT(HcN$?2`U ze!bwl4uq16>{s^=WIG?+>{gHLA^gWn*j~<8=$y}xCIpjO7_#4YTH!dGH*xVs*?T$G z;;^r>xD$qz*h+k9a^4V_g<;=F{ArBA4n$gxp8jr(^ye_fsk;QE%8@^Qi1-mk&zD7h zvX3o{D1~VyKXb(8cm%m5MOD;8ZMn*8YXHPs-9T$@=MULpC!S*1c1>qS!U^>Q3bgLTsHw6AFxxX-{ z(&Z2*IMNyaVa&lMWtPc1n`mmmM-rtdf?O13w;a#hiNf6=%!lt?q=!7Bc|GbzfiT@u zzFsz{Ha(a8H(|Z!OtY3K_&t>U85&w&rpDv(Dl6Ms?ksi0;OFM$=`@mhI|8z1f1C5_PL|bNJ!>6){%n?*`)Q0&c!bpS6}ZP~Owsm}xBBF@3hK}xjJ7CzL{JsXMT(1v zw%E=*{;HRl$PN{wsR}oSvxK%duTQpY){sCM??wRoO+)AehAZbmMC{N4Mi@j zbBi8b5~`S?W$o81w#Y?P6e%`=8KPG-F?Ipt*8qwHRgPT|Gv3SKk_fpZF(UsyQWVg? zkX!*JY6Ni+8Ji@qjy+x>+X|he-iA2RhNyyAz1J|^MWR%cKJin19%uSop=2gKV&+_r z#aJI|eKpAB7FnHZtW<_sVyNU#RbDj!ry06LUpKCmL!RuhOO9ZHj)Isi@j zMSECrhdwEwg`Q&ViWs=7N-3}*+u-9>s1V#MENxFD#R7FDUnn0Ocq;AK#FM@*2XA`p zS)Vrv4@T3LZz<^G=2?Fq^=Vx9KJtGb^fn&@RHrGx|1lFR|8zzITK+>*0{4-i_FC>r zJnkcw$6)u70VLJFf>43}%86IUV38W|ENkeXOap8zaTpkFc+Ih_(v7Bkzn(9bll^uy`|jg{#<(L8<$0XN>M4f*SQPPNC>lOaT8O`^p4&u2gt@Tyx1olc{hxLx& zxnh61{kL&<@nvt(H2-eZv_Q`&b`JQQZ<`jP^jQ!CGr_H=?5eM9MnKQ}PKZz3+Nh5( zaRc7MtnOK%SndnLSRemR_)`JtX)OV;6#FZ9W6b;XvWyE6<@q_QDvtpZvwgz(L`+&# z)u`4qBN}Gg_;Yve|JY0J@$qnywl>W6zX!hP7Q}63&-7<=t9C%@#?{1trHLdgfPHayliJKV*76I zkbQPdK^7v9^zw*uEW%R67$lGpA{;2s{jwzv4r zC(w?_MK0w6fQGML!69d^*%$gVq3K?_vnh|PX-Y{a^SZn*HlwPB=O%n#2q11td=0NM zYnIe(i1R;`^mX0cd~1d2u@Q+tQgf9dRcO|-DdJNkxMi72!~5IEu}VMovW*PQoEUysHQ$vX1uK8=%}l$ zPuJ82c%EMd=Ql@>P6}eaKi8(BJ_>>=BUnsTp{;6OqZm-bQX9zb zf8FLDQ**66O>7S0CzV!2hbp3+r}p#yR0H~UiT&rZoKa<|{DUrPG{%LG{C!6J#>Au$`&3!}xm1YfS^oLI1#z8G;FUf`PCp-q zrtNfSwA5#6se?Wb^m-!gbPHM!4fu;*SN$$y*KFVSW!~0*Yz!Us31#KbKkvXVc#*X9 zF4tM%WI%EvF%y8SAKmifbzLv`9a??sNvn0t5QDLMWIk|rpAB^*uky;yTzMl}1vpSt zo>bh8F9>lpvhb-9*emP@8nOWaXtgF$r}()i3F=6}%?s^wuGBb-6{POoxCCnA!9WH1 z;3D~gHM#Xyg6D=$B64(URoBojUW5e(li16ceV3OeDzADdk%Tz12g0!Aaq`LskI-r{ z_G?fIWXE6!7ATCBA}tUtGVWa~co~-dYj4i0LpmGz;iAiB*T|O9;=S_Ci(HY52bVG; z<@+C*2$F5`q9K2=o?`*k0%0I~W>2Yda4ceNn<@NG3da(7X$oT6fm+Sr z3@(i7COP?m=cOB5B9?#Jvy>WQ0}MCGjgkTFxTUBb0;JC;V4t18Zo2V&{3~VXT$7zm zgjW>qV5T99dsK6sxJ#Aa!_Y|KxYzfn-aqRCS>Mv21?`Age`SnV0eVhywq>6wxQvo& zu9RtS$FyR?*yKxqlm)?^sV$DI>JUg1KiS`gRR?UF`l__!2~r6I<0s=OX-hG+0_1U7dvBYlS+grTs}aUoy0^2e6k0OmT|O*l!EP7gJf*-<6(LUevcg)tq%8R#G|Swi!nzV{lyv3$NU{zCB9 z3}6?TAD;7dOR*f03c?=E0t_xIh7tRP0VKI~w19xK)Esv;HN3p)s7pmx2Tvto{q8R= zl&|VVbeSrc9Ig|TsxJFXYR*blFG4kpQKeJN`F3oQgsRz(O_VvZ3nBYkm2q1V0y6U; zET``3>!Ve72UM%*GgP%JYBg05=U1Y4Rb#?pL`jn)x{w*?YIaqvo`lhk%08<BH)>LU=5oKoYCg}WE!CXdAEs^2iG!?b1(ffQjnk-GF~t_`~MAbS29j1tg&!;9p$ zm~lzsgrac9YW8}H`8cR&t5WDO!Ur?agrc+wlB#3NT1woUIKgU7@V?vw@MxzJ!NRzx^oPiky>aFOJQgC>^R1Exf_+-RQw zT|nGZDU_FdsadHO)=bLN!7(4-5g_bTxswp;ElKs<9{h~BAE4={IWMdzYCH8rGdqX6N`0WJs3aHBcZufzuiWC5o$@K@5p>pg%PKJV^@`?^rvB1d|5sEx* zY=t>;%#h48_?r-~^8cvhzB@Qg`CJJYIsA)Cur>4}#Yi+sMH$OP0}akDg8}M}`i!mJyEZug%5VQD z@h7@D7BKq5zPL@7qD^vZ!$Q*A*dhkSKZeQE#(D3%c4n2+kLzH^V(GRxqV6{#OB#IFLtaA9xPS40k8<)@mD8oKZgjd zycK80P)sIb+*gId9LzRJRxLII5iI5wT${R=Klm%HjE7+m-23@oIAW*m3z9Ui=7}#b zJ3WsD^e^lSvNQ?j*e@{$yMxC%ysIB>CU~Lej__Nx3`FWya(j~PsGG&4Q8Mv`(%jfy z8EtN~4o=K)_eQRS(IgfuuH5~dj7G%b&r|xMvjy{s9HAg1>N0ZJDHEf1TC-yZPM{K- z{8E`wQG}FdKiWDAkhaA82YWQAh~c(ONZ@Dx+&>Nu8vBd31Vw+mm3pm(F$qan4LoGN z7XEpBJ2N(LFv#KD)I6C+^E|0xU79+KdQ#la;G9tw4|3~OQn=nLDT1%=dgRU!rFhQ@&BbQ!WdZ0RK^*BsY{9HHzt@J$7wD2cBivxWSG}|KO+{BG+Ct!blg2 zO23`hH1wRywb4Dku#`X782%hXd5}-L2+8~=M{5w#{#t84Pb)?Z)dM&hj#%@EbT}L^ zQ$JVu`(^4aZI=bPS;jiI#*s0XBzritJ-*C+PQx)S^1q%%QoNYEdGZ=8u&I6*j{`0~z{x&d_J< z_km~Cp_S)gUx8DIiyh!*ci#0_>bPJcuQKhqaAxX$G;K=30L{BszaZQIeLqo?=#0bP z)W+khwBz!des%P-j5;SA|Cr>P1e+YC%d6KjedlxZ3{6WPzDbbEP5Xw zw3oam<}r?94=KV$%%=de47u+n`T`NM3y!M9&jrUVMdNH_dPJa%ln8K%k9QN}mKx(J z#4pC>hpy0=6^64v;Zx+(*>+b5rPuDuxi?^W-OJakaa1pjb*bNv*UTDi{G1)ppCk@RcRIw5hm*hoJh-Byi^VkF;z zoPxXn89vc`L%r!UrZ#W>Pd>6J%A1Dq0{!Wbvy1Sq1yJ$qO?AWWw5yr-KRuY|Bl}Du zeg;+q7_Ro!@dx(m`oQ?nGFu_NU8j*z_!XZA6#F6g75IUD=5yz2p9b{m6aTKCu?-Mj zHJo#vd$x?8tK)nm68(m~5Ip5fe=|ILGqj?e(`+f5b6|T6fckXv{E@$F3KU#@%(F2R z-L3Ki|3o2|`K0w;=`G4JS-2Lfwd(ubU9!6!*>8#T$>aPRt>kM*0b<3espHLuH$itZ z6w~GL*@FaoQ^V!)gf}7bQY5RRll)qYq7DA??;B&iK#Tq<(|;1O+S=fA{=N|z6L1^p z|9#sXeS2^(*wp(bU%6JXU#>HiskPowtaY|vG-pI-5R*RrLT2p%enX{jX;;757FgBU zr^DZhiqaqatC~?>>9HE9O?9co9P+V&nzr7n{v`gs8GZh(wd(!DaIET&P6+0@U}Jqp zhN0g~Ni)TNR5N(Uw!KZAVt=oM114N-AJ2g0(=Dy`FC71o&j`i**+^=cM&v`g@NLke z5A%Q+J<;S$Ek^y7lZ9i;NxArD_-x~9(R3T(ZC*5&?2u^P~~YE${at4Eh1-F zu~JWPOn(52Th+!1IMHf|^YJq%LVH$v#-Gl807LHGrqyw?xV082g&yzZ4$FWRWyoB6 zCMmF&p;ORM2L$52utPnGLWmMyI}L3f4k1X!`*c>Gyp z`jRNB@a*WXFVs~29AFN~z=>Y}kSowp)Gw!G+mrUO59?+5kaI*LxMA(E2?03<%W{JI z$KEqcUovz;yX40@t1AANOi+=S8}EZ^O0pz77+vXw<1YNO^C(woVCp7L$sXp~N2G&1 z*$JsR1AQV{$sQh(q1>uq3u8i8Btuy+R28UNz(=v0a+iBkS56zITG>;CtWc2)@8Ez) zbBX2sw-t&=YT><~7^&h4{CEf;_!e?(K}Xau)%cNp%s0uX#ZW)hpl8}K%re{9e+N5h zmTK7OO9Oc7cfxR48Kennq4IeJ1pgwePB?@M$Zq)xp?O}36$(3d@ zoL;b}qS1%Ju9E3Q7vRmX;|_m=*f$;*z-^Qlii=-~y9Jvo#?B!JgAWwg`or?&_bNEH z&Ez*iyz)D!i{nMU5;z!AS)S;JR$+B+IKgTiVi;cx&OYR+=>B5BPj3uJ1GG#gqP#e-zPqImV5A* z_S|Wkx);vp$xZs9wOF$O;JZJ>d1J-@4Xd^Ss+ZPL{DY(<37 z)u@QA!3o>mIdYslx5^1#a`v2l|F{~Cmgm|c+(@R_b0j4%?q0siL-=a-u#-u;X-in} zxkzF4fe6^lmbZfY;!p`bgDk^O9;0qkhql;+=BU}^Mz-;5*SDiq~0E{sD8M0 zFNjQWOxjm^u+CdcE_E@`ctdYZ`1BRHWX)RH&IYs)4oDc&^T9Iifj!u0XRYXR3)&1I z*qYGu;X6`6HG5?($T9TG7@7{fBaLK-TYnI<&gH>Rg?;MokKo$<)C4z)D4KCUO=}kG zD;Bo0QuHSln7HAl|<8wW>fVvGLd-5VaZwji*%M5p)awxj(Q5a zjxjlYAx;w#kEZ3pLNK#Nqd-##s)gd`U{1=1>Jc;hLVVLz85@E;V?xsmqSn^2vTNN4hf2 z#!bZ7q}%gd6@W9(adda0j&HIq4VM33xo}+3{Tr%8On}uy(dXId;yj+LWYwTMCH&he z?~?x-7b!;`)Y}y;aJPg$qHfYPyG3TtszmkrO%ztz^iU zV7L)q9n{iN_c4;Sd7MCl?NE1dDx;roTo8!D@0w!zVgw@Fb`WQzzKG`HVxFwf=g&+3 zZ{`PxUm~_fp<_@3zR|y%@ca@!b+>nJ9Ysh~y&32BL-M~?w(ZG}YMPz4FP}a;HwgqK zwl7`6K16ZugdZWfaLI-9i@}>Nyg!z{b>-Q<27xTtL<#L0Z#aIu-}*)bbH&9nP1;O< z9TmHmb*Oheq-_GeHU=V>n^aip4{`~>l$e}y4bFoWbr~}8kgIVyCHINO_ z)H1kJZYxu5zTfXV!NE&>{GWkYIzE^d90YPJ6(}npi zg@mG=_?@2b%)ibg^t9vfbbq|1hBRiElbqaL0z18iP5ju_I{xq0>5tcj|Br-~k!xAF ze=`UKZcy~ah9PWe$GBsrrc$JF#v%+ab_4>0X0^MUXuyfi)80?ZPZVakV@e4!5>U}T zhN=81kh zeYsA@uAX(%?dqBA{>4`M&kfc~MO2;m{_Ls^v6}ao(Hb}V%`_|u^*f!>^-^F+I}&NH zYNLV<1-bu@_y}W3RTt}jxOf;GeSbr}u^H?Vq)A%0+S@*? z&&(OY1G3#P@S9=W+Ou)1pk5P>IHlJ9xHh1ft@0^22-&q#yVh{vMpgjnodMfPH}u`s zdd@yYXjq;8;cQf=Bw{Nzelbp;jI#N+yS4Tzy(g{}YQbkMKu%Q? zfc;6|OkFj<&x5U*QB!^E*8}Q&+VLvU{N3PRsN*(1u%IQNCV&HLg<16eU(#Jw%SqM> z&tf*e&YBe8hL#~;JZWVUEIp$>>ueM!zA>#AuYI5}}g12udqApt2jBhg|@>|Ge69mVvXL z;zGYprRe~2E&kcgXA8^B)NZY}HCx}2-yG^jNTftg>31RlgsvrJXOA8n@nLdK>&_wn zA%}PV%cUPaquWm1J4B6T-bC-7oJHc2 zo^=q9JA9&y`{G?#yt$4pzzta7GD!ZDrvd&eMlNpwx%x_-KXz`uFm;Axxaw}J*~zbj zb@9o708B>#oyXdtX~8oWyVmiv+&?wX{?u}wN*ym+`G=>|MnO>t`R0WmS$yIftVG;K z-TtT$C1n&oJ<#=W?1;{QHia^Il?!NE#O6D=^X;dm;&xR2c~itB-I7qmmg1{*RDL1O zv3@ohSD7Jd4luNMIR!BJ_#(+6K~k*GuUIp)C}@}RSNA%y2bW+ zp9E}B{rUTY5}nmLEHF48=aq4fE1Cl7sBevEvPenpp#W4stH1su?Kf!tiFn^kw0}a^ z3ysr{E-c_QOJmLrwaw75a<*F{t!JljuKPr@Af=M8+L74I@`J-3kYI|t;cAN8ki##0 zd6=PFNP+St|66JxdHK|xP44Q{z)E>=|1mH?_J zt@T+fsKb*%&PsJ+&$P_ptM#6|;H095MulZ^WsbUc4URy`-t>+Mm_vi>Me<@20K$QC z;))xUq|wk8BT3mJb^RWg^9?)5{l15#C_@n9Le;JOGl$zr#_6D75+WgGHxSQaUxrCd1>DtAkEfZgVkTED+Ald0l?Dc)98fJz zxR#`wA{+7Pf?y4vO~#^6TGT&MGITFvpmK$HCj&*zFpKk{8*G~90q3^(xRAH}jS>FD zT^DGvj3Mls=L2&*N_8EQ2~njmY`qvk#;16kpwv>t?VLMtKZGnSByWcVQ2BPn8x*#% z9mo-6eeyTG%xQ%5-~qiAq=&r{%f@Q-EQD6|GBD#wq!}dm~;n4-rqa z^pFluesvsp6>SfTIkC+EvE_mnL7;7M&nU!>((|d?@3vJ=JgdI7Jp620;D@3k$HLBc z(cae2f1Y$m5q2hLR)R1y-^1MLZzV{^J@F6bgfH2dYO5<>(Zn1YYnv6^>_DX`1FhM%Bo~jFQ?EfgD3*6 z)PTK{oYKZ~viqXcyFgy-lvmc2=^blc><83LBv1xzSNDaX&)$1pvWiN+n#d7K92dDb z2MfdMXkpmbzWXY{cg9R_IFsMfOzI zF8`#1<7-jDLb8pVBSmTy@B~=aV9`Q!Ld@FM6NQ`g78$3!vf?h}HX{279)nuj=xKG! zOgg4dtDUoClU=w0EPUgh`uL1`n6tRqBb3@=^mgp%r1F>>roNTiNRS%Fhehd# zAuB>sLv$lcSP;Yoa4WGaCE((0E1oM-9;DD?*FsHPY9?&~Wb$Fjb5^2*phQ{B%1CC6yWUf)BDB)ER(*xQOT2Lk8+ni4J0*$>_z7J62Ma?;p5&b*oLPIv3Iv z18vtT)AZ!EoI~r6to|gHg|>!fp!e?<>7DkhjW*^TgG|cOL2DbBdxe^X9IK}e!Sc9= zq$N4a(_{2M1apCWfF`Czf_IyP%3-y``09ZyT444=@CR+qLNa~ofv_?Kkwl>j)lRTe zEfjkC?U3k*l`XQz0CMM=P#{xsLmX+rvXnFzhK50DR#P6F^`-N2T=OxD!3TaU1VLF> z8d!r2fg~WYp`HawtIWZDwkEP9NBvMBgHGD`@yp?Pz)u?n=o@3JgY~r6X{y8654^g9qEd8 zEF43Hy!1))yVf?oyH@!JTb7E+bSWJXrAfT>WhA+$^QG=RD;q)8*r#bGR;Y##*rk2)5uo)dClctF&Yoi*u3h)c+TH4!6H>l+icGJq)la<~Q zK&d*I-ZMoE{!IeQ=$YKftX>?^MR@HR~WCIW>SvG-oLxA zZ}Tk=Z!{!uwI$(sQQ&F*AA?@~@`LSr6GSNwx7ViRK(J@1({M~qMH$^+C^UKbs&7+& zm}QTQJi{+%CB*rhFd4&u_=i0~Fg z&BY4_PP;kfFWWt~g8z*XO$2BExs(Xcj_lAv9rSnZR6|!TQAJO7hS17w5z~r&U^0oW zY)YyMMO=ql-yp6zQ5mLIH4#HhCkP8CK_luytT;s!Dk^@|?0|f5v=5qC7|M->D>8fg zfnB}0mF02dW*<_?v9q@5O(k8;PX#+PzatFy7926Zdq=2_@7!IkiUkhEu%AI#eb^G( z?#>2lKI}k3WKXaFS3{4fQy#dPv`JlADT=d|#7m&r{KY~r2i5MaU6YN78Jx_=N!hn% zoj*of>B521fsPZS=g++z5p48ZZx|s{Z}`IJvQeJAags)}QT~5)JWP$^&T7_~lQ#Tf zv+ZndS!^*fwk%?Lkv7UhI{-6X64E9rt@4rie%GY#<%gcsIe6<9mdT9>1Es1Q&BXSD zgAENU+Xn2^ZXMYt5e_vNap_aCPJ^PD;l64FPFHvKMRVr7_l}eE(9n_YNG^Love*7u zh)qev5Z7C^@MzyNu5h>lhXx!^41CLBoH3a)p%W7e$T~j2Gi}?OgH|$jIvwI&g5cnI zx#XiwuR$wA`$x2mVqR^HJ@&hhX_}#-!9T7_L(`jvw~w>;wfZ+t^1cM_@oiq}CnlQL zE02}6U*`pJ3K#sAqIL~n?!A_gq?|ys&8aG#lfL(ubbS$&x01#31PaOn@ z!=Ujv5_0%qJbBzW^r2+_ZmH$7{KzCXMQHTnmsJ`=o$;{m0y{`}2)|#! zz#K$M@8j6l&Cftlqkd4apGu!ANL-zvny9_9YWWjrs1r;GMiNyVhMa(y(DTxv1nBxbjW-CSI|}ZmFEw|1!5^t|}$h zD>cgSMl3c#sC+-dqh^h#gQC>a!A^;%_JL$m!VKu}F0QD5QN+`WdZsAzn8M-H4;cg| z((#F>oKRUI$lY|+g#P9c5q^0Fuh&UT?IY? z^gxnZLaBPt8@ImPT%3o8yH1O9!pDoE1l_wa(pa`ev+=~isCojUaQ-i7KAt!3!Gd@Z zRHV?)Avs=(LBWrr&m5O~b2E%eI7pN|695X#KQCUdyT8*VEQ+}UcF>uea1=NCA*q^Fi0P8vFN^jlnQ)5aZImJQ7_?z1Z=z(cYzBML$ z8N`?8^#I7)N=F`jpVvv{@gWj;4?^q%J0b}~zr=B2zQ~Dn0dd8`cX79JI0uGkLKaxK z-^y8ofwG>7n&cT)w0(~X(iGB6*fCdngFq&Wm;YMJ7|xerl@U|DA;+Wwv`ttgZh39A+FOCr3esfI-)46L=ct8|50KqmnGbAgGol4 zux_EYc6Ga27V$GU%gwb*o$XxZ%+ssMUXtQB&2(qdF)cemg;d>mO6F70WPdu#9f|=FOqs5 zZ&#YqHgd;qW3bUF-<`WcFM|7iUAU!DNpT$^g|E}r>Xuc$(Mf+NNyzrtb?BVHpA}>A zw4LkrH&uG}m(Lk7ujIPHhQgs&^;X#Uk_OWkfJr1p-NJ#;}&xyX$Bo zTZM}K=uje?)Po2^`e+YMS$}0*zcp=R7Ba$w``Cv;;wyopA6eaqw9-fOkOI^9YRX|* zD2`#MfEVqw*quw&McugbQ32!`oOo~fV6FkVULUbx(b%O1Q>DHZg*uT{LWO31AHObz z`U!eZ;o4aW%Rnb^cELuPPzRldzTMrlw0Ked__BMW--^3&8dx`?iOuQvI-u#$hd)eO zuD8~_hpL9oM&DaQcVD7&@Eknt0hgsA`(d!trva&1o@8EqgsJY`N8!+O3j%?P@rb4%4|97#tFE!#_28ExwTa=YGB zm{iqm5Aw4eov|Dk<0V|d`4=K3CO^DkQ4@!kUKjS0QIx4FL5bS1sS@ceGc#eQ7(sF4apM>!40OjM~9x?(9v_^BBi<ZIU zzwPkQPV3hYALYfQ>4Qbb)&cM#7_u38Og%)qE}=>BL<1|YT9Gk1m=g-N%e;rE^*9$x z&$FtDJ@QTqelhG><0QBGSVMzob%MX9s+C<}?`j29a1XSWT9t#&O;BG)cY=Y@+&y6N za#_?Y2=2~e9g2rT%2jya3_&XboCn~nkt11EF(X!^mnG=F&cO9l-y%nN?@xjS<^G3b z0l*h82;4rz40J-k%EmK~x~aNvK>U#AB|G$R?W?H7N8;(@`3&6sK2bo4nOcVVWs5J>+Vs(BEX?OMnb8}1gam0aY$x`I zK?!xp3)`Fp@}C1d;lNpP$Z9GUESftTI}}^k#S>wt8n-F`wq$ya$W!lOn84#ur8wgL@U%?E1_CM|r#A+FnpLXrn;dHY0XfA<9A# z8%&+(n^w4htT~>LK2=MqJD6lKz?`>mB4bPuNj!@1Hdk3>m`hHM4#9so*|7N>u{I)o z3=hpPTj+1)b3qMULw|2V<|sq`wNEu;+FXsg$seFiSP zNtBMO5N)PnHk9Zq*(DpOz<$B-*)8D}sFKkWu20Nc(X@1GP)*PPiPR(!c{(2wnuu}9)Ze>ztmBXc_n(R3& zN_7#brb(N~X4>{IP`iqBwLqy;|9-IhYA$CCZl$Lo!^Tu!59XKG$>(4B@DFC;N26CoHlQ3lCD}1N=ZYtK(CDukW6mwCZMXyPL?M@E=k1l2x2&$~-xm`6t>->Gm zYp>Y)gCc==#5;IoBhKC*zkkk0f9P_qPdZGtUgBrY@3)-n`SKT^`8O^Yl*fsa>s}7vR%xe-L*1bv5zGOcWB7SH6b@8_|-WMmYtiEQCYoFVmN8URX z=*l2Sb;pp(PJ0~pCr`LZBYI$LCC#|P4=2?Qic6r9b8^I9%`kO>oDarxpp?8bB5+O4 zus&o_1tIrgL!MHqTLJBQOgS+llo*ng7@AxW{zi7Kxd|=yr42P$3Xi4*V6AEjn&eKl zt&gzx_V#ur`?dDAmv&c3s#qT`uEcyWXyWA+)ZHA!etOvN=Jay4DnoVag(>} zjxM72`buYUb#uC0zw>Thq6WSSW%0MAI!FdnlmbWXqe}NH|JqjA@4e-g2C;UGgp_KO1)z8;=hAP z-D^rlPea|Wm3$Pe7sP&iXa?^|mWALzNb5`OL6rEtgKcMRzCPHPVum;Ao=QO1px9;+LN^*!JbnheIA3U&W2dktO`L z7OZ#VuSweLH?>leuD}<+SMl&@(_QXI`?o-xpjqlfJV%$Aksj=Kaoz58bZ48ffXRFZZOd{J#}nCKP1!w?4N)nW=~n6$eCsy4ux z-{yg>Tk#VSmlIVEZi~ZV?Q?qycge87oBSQ#F`%uY?^kh(^SA+g^R{4}?ec_gc?YE| z$X3u5uc889Ev7sJBfmPu<@P;BliG#--8wkrHoj}>k|NR|9x*`6&lA%xXhSmlM1_&d z_yyA4uJtNxdr2WgMWQj~vAMhP&eexdtzx*01xG`JBxwpPtG637aa?AMIpf>Z?x5r< zxTlnUU0X&&;s-$yDfhUH-~?bzL2@wSnRT&T?iK4@;}_v$a2JS|XqO_#-Q2BHa6*da zl2w@vx~qZ{z|^^z1!j&=?-BMXZY=dLYWvoX-SSm zFtULz@(7zHMM9MtS!Q*(sALu_Vm=JMDM_a==Sw(X3J*Y$iS|WyLQzsUG_O3N0H<>% zEq3AEsN$Z8WUMJpfgI-@1W}?<&;m=aF0DU^4KiEBTpamUFk@bW`7Od`8lk!am?A~# z&PD$tuCOR5A9Clt(GJJBsXoC+LbDc4@(^|#mJ*=0I~Vs+^c2?+Yv0pb^tmAMtes|) z%^w^D>vuPCOr5%_rO=dZ^j|2A){ePX_;J?8t$UHi`SVMHg{#_M9HYYUj^ntSVbxKz zYmnm~uTbM|RB_5uP$M>X)%BzMWJ^jPe+I-1BQa8)BmHr-5%dTtP7m&wq)r>0J*Y-U zL566LUx>BE%`PFP(Yj$O9%wEpZF7eUaZd|&;Li|=rOU&TLP*S~bdJYh603kysz>?{ zulzfL%gk^TG1nA>FNE~BBInDhjLia^TG{|l7RF}(>oxex^6P3G#x!8ko?4y+?=t1M zG_9N^5ie}2m+5Js31M}kQo&J{Vjo*L0ZmzM)5MrL!xRmd0NJhQRI}&_8;f6g-jq4R zpo~;}fk~-pCMMZO43Dd1{EcX5eoRf1Tg`Bm##*NZey3IOXZ+RrI!B5s>*C*S%ij$? z476k4XA<)TicX0PiO8*+ZnvTC8o#;96w}kH)iQ6_XVyY96p9z*^6}EA@azmTlUPzp zZ7T6vSsK$L>RZF{G*5-{PP{vUmP%l7{kGa=Nd0qp2V-s8B|@#3R~v>CFc*1zj9d9v z2`PoY)(){t9!C8^Rvkm-{LcXD$~c>jIp&2PGtC+r=OxY^B@0bOo%OLAl337o4PU1i zI@K(*5(G7N^*nLzj4PGQx9e{QFF@dy#lY4b{#{O94I_P>I(%s}_Dj>LsQGBp?RoJ< z9t8TY9=(K=oVr~DJNXn62f*a`)U3&dV)UJw6GH7x|5(1OUWw3`?d48oa`&e%1bgVV zJv;;^QlYa#MrW1GXIS;ot!97M_Yafgr_E`WC*A!DDzEqnYbU+(sIX1pkGn}6`_Mk$ zO?wvvc86wiG!Bdak~q4%;reOGXfVOZ{i6uV3^|H84Y$&p0qXk;f-YwOxugv~d64y| z+q`)|+P9P|vG-2lfuvim-Rn^=?oSdCQPY;7@|VZ*n>LuDB(E0$+-Ld?kz^#%pb~EV z$XhI%)XQQvX`IxB6>8@%y?Ut^(R~VRW4FiOcs1VTz4n$xk0Fy77NT-w4@ZvHA!cF6 zRFkZ9vyFVCcZDm;&~KmS04#IOXtpkZu_JSVC8N*%W!y-twAcY&n8~0;qnZ zV63NVMs9iS_-Ih>BEPvR%BDih!pSV~jLXb%e}xy%4XBB2M)WTvx=S6Zp=SO#a1X^ia z8etso@Pvn+3|+O~T`JJx+bGYib_gENY_#0RblklzTpT5ouZY3YGcXE3*~b%^&2K7> z%k#8N)l5d;(^k2h(^lD)oF*vDE)F?VPW@If7gl%6tR(m4`bAQidnX98Mfh1%@!M5c zJ;7HuW4@x2ReyY%>{VsGqJ7o)_fIRcEiKpF($A}_IUAO^fi6SjVGmrDL=E`n8Ri*A+}>d;xyP?Asr-dr2}Dq$!=Y~@R7~Xq zgxqp{Zb=^H7DKFnj}{7NR755D0b%~g^gKNO!g7n3(3JyuUKs?AVTF3s#O%<2=8AlD zzRb0DOa0hhxi)&gyUask^62jt`>CW=_hzxI3i^pYczbJ3A~+@5vyr!0zX?{9e>r)a zwF%ZH5TZPKX{Gzy{Cwc{as6?6vCM52UTsTzs1XrLe%;eDQYV+&5#V*ZIC7j)_MFEs zTZ0zy`L%IC7dSG? zQ#?q(H#oe4K1I!XCsp<`9bm!9kE=`*^L%^8sy~Q}*1T*gZ$b_tbO{{CZ3CVaAd5g1 zU>)YjZnkrm+iIYz_w3u**vzE!SWBQ8{M6Hi*s$sO=%e?V{M6iqSP%WEtGjvY%0NGd zu_ z4fQLj+DZGI%FnqpW4*9z2li+K$TwE6I0tj-0?ett?N48cvjbEfvajd8t?>m7>2CIw zqar=5Kvm}KqO5A}H40nrjSC3W(m?KizM~hQ*2jS~ZP-3OcLuYnxwaDY>nikYMg0(| z-en~HanXi`~4*w~Uq{WdLbca1yAq`~2Aqtg>5h7ETMEgdmNKjUE_VmOy zM)xKq#lZvJmo8PsnMi514F=AkkXm_=;5-i`lu2$DZ9_!d1P&KSShk4#&O>7FOLDyF{S*^C7XfGElyVtm!V-wbgck|x+~7JRmtu_Ukb-kbFw)@_u~|&7>HwoGxB})Z)F~Kd{db1)pKsv*f?*u$ zeUpy$Q=heZNAB!Wuj_r&j(zBMEnYz<-{~y0{m1S=CjnYB4O5NT79IW031)G}qN%cz zYctmi*zKlzXd!oWsc_EW3tBf5&Ec^E9#O(${WI_}MoQ!wu3hlm` zmf5bb)7KqGaHIxXZ2NaML8a1Z1&}>ydh87J=sAv%&5o`M7)#bS+`+xpS(JJ|=@5c{{YvLtNCauYOwC=KjJOFW8+Ix}LZ}2TUS6Mnml{ z{=5-?1`7u@^85)V5Ro4U^ZIiz41BUMBWRkD@XXxt{*|;1XJg8n&}-_(eC2)2X2sUr z52S0!-h71ANP?s3OVYo|p4Eo2+h6Q#R)pvS(f@Rn`jfjt(89dnnlmz|z~D?U3b82+X zLG8`(_ZQ8?q-dY?nPAe}?37{w5mO)1XF1-ug76ew&K02e{F?8?j=kPb1pVx}!FuRestv zJ5lSXy|2y@?oPE-K6%VR2}@^L2VmI^KWwVM`&b&53i}z7W~(&~N_WT;$xOK)cOH;Y zG3-VjCOW-dYtB%mIC)`@AbIQ)M0>?r7x@JV*Fr;H# z2itij*1iEVeq6FV;)T=I*YI#N-x-zSn@qB!g&A{ zho`}qtSd5>YRJnMnRNPHXuOT4h@5d9d8-DWle;Z^|3*4O(CPt4(5ALdpK!;wI9o3P~Xy%58r*Ojenhm zz%HnSqY<)2KkB?^#&m-RZR-G^%m|jJPcnGA3?DRVSbR`yydSc0LX@)_*2vlUOkb^Kv6n8;x7iah>jcPsZhY; z23qqI*ATe3c3%JM+majmnP~@Mm!$c^F@wd=omuJB7gpI!2JB*Mu;}3i9#Qs zKVfkKgpvyh|2H6nEEDCw6~jdF-0F(Ll!!slQFP6%?3n-b1$ai|68AvW8dEj76vzgF zZXyt}3>M|lK^L+l&vR+3!XW!kFXX!5@WJh#>fsV((CL(P zpo=)rQ3jaDq9{uAsy5dK4nuuPD*BHzKH&|A1Vj=fD6#RMmR_erRr)AG*lBNvdyAzY zgJk~+S3mvz()mxgKG2ZUL1@=vC#07^$P@g{0TU8sC-Kc8NInw`=2~bMiP#s!Pe~LD z0)q`No`tK5H%)0(gFU}kl);>8 zp_Oamm3tg2+w7iwLfM=vQ6yn=QvDR?tKmaw(c-~e;p&mA)_KBm`GS$A1qC|LM_B~I z)#bnXVI(X#Ttm`J(Gb0zs$*l7n_qC3AW71p{o{_un1HRyPdknr-30^4Pm)z z6H1v2Zhmt~*0_%KC90s_bGXUnoE3!T*W}0WV`D_w##(fFd{??L6Ov2EYI9B)sL;4< z_kulpu4^RzNh8Dg7x$2iO22TFqvI>bu(RBZcufIKgY}15`OP9ntnflmx;n?|F{@ed z_<~8zTmAm{i~v&R*xqPe{8)bc$2tXzBOycXKo>_dQs8)h>B)wKm7~*jTBEg1B@A+V z>|#H~5|5HI_^}PWOtGd1ZIc%BWArDozAFgZ;ezA27MiN2ui_eE-(P+^j<~nF&z6S~K+@6;$MR~#e zv@i51jQT0`I%t&WGdy_8l@JHJWlYc=2>Y#&Y-p4>ENUhb1>MANM|t#Q=to3Kj*SOL zQ9tLR%jC(F;Ig^PJLKOh0{jzq2$M~8vkl$-cO;Xtbd)W%-Y7)P-o8?EcE2U*#?UHl z!c+0Gp~tesG-_+Tj7mC$m9KxL1A|HB(bQSxP`?M%m9XoZXikkS)R~8t<}W{>$0F&; za^ReTtFP|8y1%2SQ1|wNy$*PgI3O;`)=(Z)8JiV2iG%cWL%a&{83(2RikDB{lMJUn zfMmlokT&=$VFY6Bqu-MVjUI?|T`J*t&d`YJAt+Sz??v*uLK!_P2}Z8-6gp>CxHv3< zTBlDXHpN(FUzvQ2t(DyV-U22=Iq49wFQyixWZ1=wSl}92#uolhk^)HrOMm*@u%nn~ ze1t$j2Huz$V!`|%Vr7}8)QT&rEZkUTr^UI;qtg%5V^ zeolJ1xsM?o#+OP|-|-h3q~F2NZlFTnB&nN+xKY^o41A3@qCr^V31^RmQJI zmy?$#5Qo8br3AIa$Doyw(_B&{S`Bk6J3|E@q)`+9AGY2(#_vg@EEcM2*Rqk^!9KO zH5YDBeqkt7IF5gBMuc7*opyjwW6U0?q6Eb=q_Q({MbWCPzY|NP4C!0fU@#=tI12>a zxh%cIF_h{OiyfN!GS{eSA4Ea35V4Mr?h)z$Z!*S9C=Z2;tDR7Ylh`Z+TV{?f9s3Gb zjqUxhH1;_Q+sEB&%h0Lly2bZoiv7CLJL+iT|EiCBX6`{#Uo@s0KjV);Q}O@zz309F z%`Pm#ab5|A7+gV-whslj1zOW~l`7p`AR!_Gy~LgIt^k#u3?w)s{Z%kMA|jx_D;*5W z9a+Rj7a&wNIt-OZhFxYFZjBQR|3H)J(C9z}&L*pcnFL~|wzz)h{)5jX8G2TN=L~08 z*K7rpD~uKsq`?f77*HrGSlg_X|GUxhl8X)2DP{|#2CJ&2R=8@JM{Uz*kVmhp9A2TT zP;fEGu+dxrv`u2FNzcFTvqxt&IPhps+gJzQ=1mjmHW4* zybL94=d~1XEd!^3Hl}#BH)N4JcdU_OXy~`Zah5E%O7vA*W_VnxLn+CXwZ5j5u4A|` zsKA>$Hl)hiE)+T<|Ak>zFdLGB4?1D#R2Lq(M*M%vNi|5BVJ3)2L-I9+3;87T8je3piUKk^7GYEXkxplt4zt6#pPn312Eo zS^iX);E#-(M-&b5R|@u2QrdGMc04J~ML}Dhgx{fY4n`reXiIoxiA!b9isjD?tUaN6 z5(R=iv83=pT_$V_h-Wby5O09qJ_5BjwehPqVo7Q}8TXll?ZJ!RXW)w#KSbRTjS1V` zX>i{|;uhU(A%UF}f(EB#r}(m$dgd#FNq+Hyn{~xZx4FX85qgWev|D(wyEfyY8h@+h z7{B_}3$1V28kBC)zz{bKRBg7W&DEUJ@KK>{G_~XaD}IOZky~BA5ei(^%jVZ#`Mhwov2|Ql{FbaM-TU z#@m1O+g3j48TuvumMnzZgoG8*_%ysaAd5mE;+swnH$}x_wcIU&Y%+r4_;V`#p;e!5 z6Lci~8v5x>JoXu(9>>@t`p6-bTs_wq)5s(8$RhR5VBIzNWI@vgj8z)|bbrq10M1dz zmw$h*z_pHh*!7-2UL_8Pp%?yeE}X5V#~iz8D~ZYm(3JE>085Cz$zK$tO(5*@4Dn`% zF46|Y9*yb2k+2vW?WUL^4N;&}W@p5zT00Mde=D;hDhF@JgW+OR3==BxfAK41mYTqTp!~N(&E-)%a#~5C?TI+ zjrNh?z%y$7ZGcITrORx(cGr_q<$vBEQH z>j;-&PD3qyY5%P@!sN0yqtXECbkI!7j628&C*ey_2ZlInIPh1p$Gjuvi0Osli0OH} ztOK-`+*pmARGPz=GN?BoIGE~jj;x`$Pkcj6v}d2>)MPf2cRcOyk343Kk_&Jo4^^3? zXS&R=@9-X8P|YEMkz5CSAaM^4S6MjmwnV)O3r!dOb2pGEux6RLpI7L`=M=TTU1q58>uh3FqGZ zDN-BIT2%w|kw{`p5^!rTrrb$D%8utR^gDB8p!d0)jJDW>s5aCdahPXZj{cE0^+M;N zDzLXc5e~37<8DwCt^<8pLIswKtSWXn6m>BXt~**w_=3&g^e1`J$O;IX@uIo+Y5_v% z0FI8^}S?L4?u!Wg}uYg#DWtNmlplOJFvH(AZeYVc|(XLqn z!yku0Z&R-cfA5~^$q3D6(zFvX9}6kECr?esilF*K#c6Zz0@5@tz~QBkOGRZ49;qR` zs5w!?q)Ih%Dz=E8L;+bjl@PP>>OyO`L2vrJ)xrn=7+p zM#sC=pHb1as<|C>ad{@8YenyNuMWmHf*dyQZx&uJ2$jJk;BK+z0Y_E~{c7mPYPpPy>;mBSF)z z`D7M<=D?+N)?=(mR5&&Qo3Xv>1UU5;W8#fO^*p#fS6E|E4gQTp*^T>)kQNi#OtWGe z%zJmg(_s$?Q280L3a97?oZ<&>JkD;c!FYIMwqH;V z%c26M>2$~Jdk29mNF~0%;<9h;x2rOe(r1<{8V!z7P{V98)r8zrLMqx`)B;Hz2Z8JL_#Q9FHWB=oXR6d4&+i#?=G=+vJ;IqUJ zdGpwBbQ!$3FB_MduWBLW*Xdn^{)hAZ1eY4{vQu8 z(=reAFSy&+(k9>PCt@j87N052kJxTkri&p>yXCAW77u%gZMP1}EmP+fyB5k7RB}Pv zab0b)v(nFn)6WV?pqiJ{P-zn&-;pZR}o1 z*0<=40jq*^$^E*zsjvVhkz0~h#bLhw%-imnT)K5nB(A!HxF%(jxG^j4rC61~u;4K>H9UF5wcC~B?8>-0VpQp;ZIzCu~Xe)sL>w68?yJPM497E-(Dk-N4D^|x&j|y5hHxCD2TTT+S-L`y{M9liN zd5V3|R3B;nT(36srvRs0dk32|f_u-kkNKpKA-ho=inY5*XhqlRSpHHE4`}=H@UJ-) zcb*sLm?%47zrhjps#LA8%d!P0Cx@$qr3s@+x>|>=(v7t^2eXhPh-I0+Az(Xx-^M}q z=pp|4!gZ_7jV-y7(yw}-`8>>wV(t`M;ddJ0p~hIrG{E~fS}f{$j_{EU=?%VV0wg-m zm}}hM>V^ZO$4#xO=B#_PyZfmk*z1jU8@q2+%rv*`S%JZLFNQ$F3jR8}2!`tevOc5sA|qasFFBR2_2o zhX04ZTc7}%bUuKL38Oo$j#&P@Lhu;_;`04D1){H{S2epHE&=;T2_M?r4F_C<* zLknOQ;wKM87dg$nW2>C&-pUY))J=+&K>RWIT-oo1|ABk8>-S^+*1Fq?{gM52JLpCK z5&t*KpP*O%*q@|Ve$qlKAwNAZ{@?FrLVg57y@M1h)w3rdJp_-U1t-C=3wM7`rG(1f zgN5|a-i-?Qk=`R`^Ps=1g!EW@iq42$ab-^Z{QU$>{3I4XBTPRdj>XW@H%j1!W{9+m zJdf9gMSaXo-?gyOd#@e8n9QsIpON2HJIibUy@l$Go63xvij148jG3iLgvCjOrOCRg zj2(IA-U?G+sj1JF%(sNhw}#9&T-Jxy#Ywrv$$tZOl$d+#Ouyx)KL6#6{v}v#9M;Ph z`0hnO)3lxbkO8Bjqu^p_OK$@0814g~qoL66wxqun@*vUj+_l~fp6*KIN0I-<%ME^a zN4`Fhm;#csjJqRo3?z49%w5bomJoKu5jM{*`%EnbAGeF(deB0n>@8_0=si--_(buR zA_Sd3_(J-tkzvJlYFMigcs2d*^exy|`RlzIGtX;$R-W-1H{*i|dlDQ*Eu?%9*Dl1a zzh8y**lU@}`x31-GW|xY>io7@v%b#r0n|!GxVFGvh1lHgsONT0t&PfZTC*zCX)2^j zQ{G0-b}7?}R(r{muU@tJWF1D$C0&31=-_zyqf{O47D>wyF>y_yK9gZ91)WkLCBc@^j&`24yTd-aNEx8e)yQuz=b-ALH+ z?^1fvinm+*fxMpz-3jR0ctx4grgNIf^e+av&7L()YA4w$a%-Mp5A`$U+gq&oXjoef zYxt!n9Yf#(jkpwF6QPyLvY?kKhnwj%qw!z`tI_g4h%23m>8`JeTdBceSiaIcr9F84 zyOrfEw*RF0dN${mZl-e{|cY23Sl?kdW-olF3>1ioz2H$F9_Q#I}T^` zy>7y-nZ6e5IN)B8d0y@t_TIDEDPR$qj$W{96D@Rpn0ek`WLtV-OQKrn zy(sg%2x>ER9M1T_ivKD*GA`;}?gj|AU>*dWgqbo*0feXi0h#LMyD0?a^f@kZB=}cU>jWR_&+mhVe_lZ_weYMeU~bE zk!p>s_kv%c{HyFP_A%+%{4%WSs z-a0i~pcLFpUElMZd5FISii`)NAh&yFdukItL%&Bd-@>r8^90>LzW-!(<9!@Yy1erJ zq(8H|$ZWJdm%Nc;ZNJRSpYosR zTEB5>_yF4n%lraW_%$!>PY82Z0vDjRU*MV}vmG-ve!Etv$&+hO(NPhdh19i=gMg?{`6h^TPnD8#4u_6qkGtw{jl&G9|$5l*Tr$qn$&-W0PSsJfmfAYi zN1tQ52CL1u0u^eTL)1-tx92eG&B=HZIgEX~Ed44X#00`n7>$n1c|2`vvJXZo`xut* zQ*Ket7w3qiC;ehw@E6y857c4Z>9r2GfXbyZ%+#Jl^L$E*Q+Ig#Vn@+;b@V~g+s53HmS1FisS0=)^4>&p%o!xW_%Kw+ zNK$HvvtJhAHyC1n3ypdB_9aZ_DTka?@91ipb7<~WN$W?;gSuf(<3i1?02_Y&{HWc+ zuL#00xb%B64R`n;Z3AKffx`CX=K9}YUTRsh867Z)h(+RHKp(Rqej=xs2GP)8)w6O@ zK#b@cF(8K!C z(Pv&T`m=*1lr#<5hOY5@tO<)$W}3RDuCaTxi4B^aMF=f{-lY>pe`3x+Im=sHgTf$t z7!-ECXU4l-BYYJ4|D30VvNjIMy!~(BcT{l&f~!O^?Ry{$`1vyT{9UE4rb#n6pGXwm zK8OZj#t!;ZE{owqaZOp+$=659-cX{OGIykKu3t5xf>gFaaWc1Y)HU^-#el^^EavH` z+rNcqqLX0s`*wC~KHU-wh&lvW_q~Ljw&{}{x+}LZHF3xkai>L)A1G4q3NxI88QtiJ z)#PXoM*58SlupKf#z^9&lEB&t_6P=cevunBtX8a^(U%r7>?!b)jZ%pSbRYq}lnIl6 zXS?(PMJ9tg2Z4TldcAURU(8DE9z=5qDZFEei@(xv-^_v#Qd$*6Bk=9b{+=E_!ESyf z=sca3+zX0+7m9fIT<(W@|5Wmhh~Z8Sy(1NqL}{AxnD8iFYn_XpsUM?JIWl;7<#y&R zJG995EQql{G<&pos7yh%w`rkuwJn7Uy<_nNSy~Z$NIQ-WJ_pFT^wB^tqBG{XIv#`` z+Mn+a%UBS)IUS_3#Cx>?L(?_Jd9^!;XQTp~Xl~O2Xg18^kp8M?gK4A&VKf40esoyX z@h>D)v!~3%4%+~2xCVLI)#nDf$SE zFh*r5F@u&eYqR_J`$mJGZ%@eGU&^UoHtA z25JSCD?Dt~$65+|C~jn&`?1$QrB#jy+@w*8gj2#ev!qm{#VNGU55Gww@z6dv2Z0Gy z8gbxH_1^F90^%s%($z2%tt`b#B~?w1j^5Z`{*Ftv!S73GVlLU>nlP4j&VGW~%?PS{ zfL~@O6Dt8$gsJ67pYG6ytb{k#^zKR|*roi)8fHv$h(9iuFzof9Qcn0VY&!{`c)~}f z{&~*7IAPgEZ!-S)&ytE}e`oB8m<-sE$Ii@2Qx06kAD~=wf2kO?AaL@zjoiJhbVB*6 zdMln7On&Hh73-=Ul%OVi;A<;y^i75gPizBE`7!G#*HMA&!V9}u3wzj(D8hQq=muj z?74UL+}p&fvt43bB>Gbo0^S@eLUy_)PLy^2#~4PO$gG+;0nUY_aY(!|56YWds!VK+ z9jNS9V`ZB>&gA9I%hzSIpUaTs@$FCn`r@_A+qe@rL2vcq$=f8u*9)%zph`^+w5n#l zabUgyT5H(9Bgmso_3?h0aH8~mkAQx*gDtZ`>30V^$2y{r#LO6toWlglAUNiyJkHF? zRa)t!h_#Cm8BoR)AuW0xx$q2hA8-Cdf{!bOe$UtX_Un_psN&Qn>IIWz`p6@X_3-OA z+V!GVh&lRqQ9lq0%lx}o@E;gll^!`r{={#2&`;EoZv|3&$uDNIzi=^%xho;Q$&DaE zZqeMT!+OQF;Fx|gb9*uHyvR0Ot#b(u?bA-fdZ&TD4xjU26B9Ib_1T6VDMzfS3p8fx z|GWR2ow;~cAO6+>6v0g$dcg*eT=^xLrm`yv3RPzWRLa(1@VKoZ(fhDY<}ts}nID8A z@D@ek?w=oS$uqydfseg8>7{-`XNHrPIL z5TqjTdxEOq29DX=&*Rpqi;pu!1SBVZAs z5d;`a>1$OYK=F(v5kvw?B4CsrkfJP`uMEOSN=-2L_9D2JlGF|R7b zM?elQSpuf7QS9u|!kEXA{xri986&3tuGSbTiTla81+zloQa|8w6_PqfAog@|3`pVP z0Oc!So+Nk1^^sw<#E#L+{DW9rct$Hd9t0*BxB_qhg$hg-U!^7x8zA)#{yCi>ONywn! zep2KNj-)9vw6p2OaK8v*Q$!$g=+X|?TrO!qSY%Ut6L>K!;G$ws&e}4|%0mMhia$PW zS~_yPjgL$-t5YqFfnY2S2>-e%^Js(HQE)~y#=Fwf(@zJmwsXXfj2oWYT3P3oQh(sh zDkK6H&9Qhx@CmWXpcy88z}!kEc}x3WDjynh-3)RX^;VJFzk$F!oj^x=TtWxIc5|STKtn_Cj2?cGaV}E zR!zQ)eeq#fiRStjDpW`yEFW>_1Gy_w*+8rEk>4z3)%q~%gc)g;xqmw#rU*8414BtW z77CQRKrnkz$+W)_J+TR{L5Ug8AM1al1;m~s1{U-zvaGpW8e#L0qGY)HDP(iOY|>GL>GieaRVOt0Z5+ukv+fszGMV<5=xh0)bskyqr&_6 zQM00`n)%gH-s8@vO9xj67w+kly}C*+)JfEd_3~(Wir}tTRbIA{waK)^&5h&A$io(_ za1%M{@AJ8y7XYrS=2u9(8|0PM%7#Awd73taPG;}8uoMg+PUo!X=RA(z&+X>Ai?e0m z8(C$CWyYN-3b7~oaV=#MShGLVbTAhdT6>aU@O12 znq)`q<+kYVXiP^~TY-u3cj02ZI5`-KX4Cpo5>=U~c|CFh8ct}8kbE$aPtd;jf>%)84(w|D@6{c=ZYL_$x%n?z?ySQ5_&*lJ_LFDCn5nhd0`hmn64*=DIZ-r zIuXX(8I^h%@`sO2lHmm@9(p*!oIwyL=(W}#NCXcyc#u0CjFr$O6GuX@7)3p`^rjNF z1{Zf2tq5qAd}LlijtV|S8E5-ZnFti0>+gk<#ACK(LD1qY!|mh1DJLrv@(lWeOi2>4 zbQi%*(>e5$>AWoty~_Dg(bp@{zh(6lByOAXfF}})iGXAcoeqkUH=+~jRy*}kR+;lr zcHfQXC+i|#jk-qaP&?%-fGPM0v4!L%cqB;qUK#*7_3lvf33U44$u#`K-Fm`*U&2xNCuV4F2bZkjKr|G>RdO^mp z!V=M5IstfwuE{BER-dj1|<8r>$ zW_!!Y_Ry2(O^hKN9)>yIW3Ss;IkWkaWbrQp0t73Iy^&Lrt$;5qjwQ#ZDia;SkYdnk zi#H3YD2t|>>QbmFTOCPFW=Pd|$0=H6^79?@ZRSnP1#d{zf}~)Y<=Q|r+kEE1b!B!) z)rzF5uF~y80Dne`siQ&i)3i^kdYt-IK%plj)M}PbPef&OP-?@`t}>mMET#b0Q* zP-;DEn!V@yVxc4G0x4M&RO>2GyU?{&_US-)g zoBQ}A)v8`W1~RSI|BMD#$#8ob&o9-&5eNjq{rVu+#$9jtmQX8m#d(C)!Qw&EY-JOm z+#)yPJ(Rp{V?Q{AnYH5G_A3qG*v_{zL%6%}Vuj3uC)`BzMF`#tfqW$h1R**MgTzK; z1rA>)5Fr6mKxdmpk(B(^M&leWB^lOM?Cj2b;c>`l#n!YfuG{3weBpH{tZ`QDPBPgs zRsEB&Q@t~ZrSDRFuOGy5+;;9>xewp4apuOuCsBcI0w;nT^-x(OW) z_j|d_)G<8W3d1&tcCn1iFFJ~qwf?Mx!Y2xF(BwJGNB z9n#)e3J0@;aT|Y$V5t_hdAb1nPM8m=SE*V*Z>_2E)nXQ;Y7N1@7y+nct)IJDRP79? z8dxvYw!8~lRq-$O9f#KR>pKpp;#yKO0ET3UOsoE?=2%kS*4@dxzObMiT4}Y}{6x2$ z7jhl(MU5B)EDSze{#CE8WUHYDx>w&puU;3t>?P<-pP`&bteAdzr zQo8yaP5nnf3kkY5&j8`MNQ?i8TLE0~{`%s-AlVK71;Bzv)@2`nGfE`{?=x zfam)-pd8ELc0d2%#rho#IYHO)nMhR4SYmQ`wYA;5JNP^?J2pz6ymh9i(Yi|7lyju_L=;8TB>G{h# z>dqmfrj;04d0L-OADf;^j#!OAzT)a?r+vh(D|B45@UeXc+&P(=U+8XS4&IziEDX|k z59OfY3~#Vc_$$ig0}oox-w3k#<=@@UpiW-K@R>Jy{!nk*37UfCoYl zF@cztVb4(EXXWsjxE6wRYSph%u3oYX8)*5<;k*MgjRo)M=_j`HZu|0lfpz2X&iZFYEu^Df{E%4oMY8TPf4+E@4?KRs!cG69iry#JtyI!*!LaVP ztPsC2W?_#;!NUXipCHeRmt5yO25VQz(TVPOv|AU9;)flGa~0Js}Vv_eUy(J+<-p( zz7%odpQE_tfZiHICFWfa<>DPB(!k~7p+li2D}C6d6Mw@d9TpfEW^|2J!X>FmzNr6| zF7PVxyG%JatMEtV{mOCYsdCeeJcCSN!8k-E zvok=-$;d~wj}M80|KMnTbEF9mz^gHXE|XVq?IfAH55f3`Kw%wUs<`>ryWOd>2aaQ2 zqMu~wk@24^t(bL^ozW0oF}G%Z`6!LLR%gT`>s76~b+t~faLv6U?b14v*xU?$DGzg5 z>eP7s{K9Bk+3B;*Bj?h3VR3Q{UfKE8Q{4$Sxd5a-OK*Eb6r@yxbvj_+S->3#>EIUF znHBHS+AjM{vNHB;^40!c!FFlcuK=aBG$w|7GYa(LHmv~V_1nxCYnrl?2Uew@PW+}# z15UP!rn|_d@8~Q+ciCU6I~78k(y%z~i6%u~bUbfH7<8IxS67LtxaWT70}W-~w}}f6 z7G-`SQ&f)pK4Hl=@Z~w-Nu}VH2!xrc22E zP*Ei_3h|y8svA|FPh?QvjJs>$a#LC{K?? zn94_U-VYCm)uQ)^F85izC2zgu4th}YoWXKg1C7J+H%u*0ofnKeQ< zy@|XU@S8onc{ySr>4U3oAwo)<5_n*>K8Cj-g@u_NwqdKvt`+n%1*4|dD)a|e>%>N1 z&xm9x>6Jag5&Lj%RbTjToDw11D+}(+n@hSlWkTUoftRi;yy^NFrnL#oX}f6NytN}F zZbTukEmW^&WU%w7Jc4BV>pP zvI^Y6PN8lhN%BdC&tZ=RYB_r@zDrqzxHjep1vgVz<+6%oFVrfxhum9lg2#{HUz)^kfnRjP9bi_L*fyt86Q_4=)AL+bMu?Y8P!k*w5KR1mvV=-uMp324mLZUAKpWx z&_^_6>$8nLGLC+yD$&$6b`3jb9>Gpr(Ad@g@9&SBdhej4ESj6fu917%RJ$hcu%k|z zoxBnSpV0>dK#S}6fmKKzb~BBROZJFb+5Kyu-BIlcPmd=!J`@PweBW&X1USB+W5L|$ zZGzZ843uwv@D_={o5ULodsz&V4_W0l0jp0ONFCT4%>OqOK5$PFl>hJl1y~TDus}Ak z?1_{4X4fGwB0(pC5$?ztkiVFm5MBJbu!7~n1Xcs;aS%h%iFSh^q4}K?)XeQe_5B~9 zgQAxqB>)ft*ul#{=0USqKsJb)p!sk@RzMlaast*uLWGV$Fbo(qEYY@VRcpgrt-?F9 z0+*q_ata{(NELpAmnUF@MYFAp7$XG;u8{0zsG1mYN{TeG*w`f{+>M1n9v{#SWG^;oy31a#2Ah_O=g?Q$!R z1aUz2XahTpJuANN0}OFMTk(_n#`YNU2LB)WYqnP}_^&&`6Mj0r;Evt2U~eumi(lL= z_XB|GS~Hw<68un?&UPp$1Vc01hmh!`#Dy)knwu+Mlb?9dV z+0v&wbtrO)!qTTPHQO*G_}at0DlCgJ5XI>6LCvbh_?Nd(dmh}|rreJ8h_zQOuWXFK=67?j3@h8mxa|y z^1R(uHlH?!sJ9^!EC#$`^lXw@+)WML6t72HSDWe4LsM*07tv>5MRN=we-mlJEv(t^ zec#?*!rJR3?TU!soT=h4>~C&MsLMe6=aOA^7|}xQe_v#{RINu-c$H5$OL?yyCb}@i z1*|VV2mB}UBqA_I@$uH1r*jtGuk2474^$yESa$ zu9dOs15gP-BamK3N5i>uCfk@97MTXh2a<4FY$y=9hFVMD(X*k(8CzWCb1YK+oM!+4 z)k1R%w3#r+$%vVH<^ph_+(2Cn`u#0OU>)V@xAI-My4e1|wDIc51ymC!g4CL4P}a&_ zJOYj%J+K*0{${P7SoCWH8R(vdbv)(vY!lObXyBP}_BqBMF{X}a%AhEaV@Er?bwdKbq^)*Qrm@unXFsxTV#u@QSKwgs{_+)Yf z1rannpA;70#k)bo8v%C*ZN!ocfM4Z++@CTMMzFjqjYRE!!#*`pzOV!eP0IfzGuyiJ zSr4s`c<9`P-xZup3@|NQykY6guglu{zbVw7w_hdS1b<3Sr~GiWVcjplerp#Lv@bH=YcLe2u6DHW> zBVj((Y(qs{s$G(9a3ZUq6Gq*}F52qjN4Yh$+u5LT?n&B@_8ulSHfLk|>sa*7X9UEK zPfVN|9x^%Fx5g$9s_o^k>PPb>A>r8$I91xaJgV)*yXr@Ufks<$)8jVD$D-uX9MnrTsE&A7*AY`z zQPZ$MvkrJd)69r;9IiyXY6oi(U`;qB?sJ5;UMyW)c$2(w@}XrD3As1!{z=Nr zTMlH@T&3{)5~}bX3XddBP>f5Ytvt%oNJ(bA_(5t=x#ZyrhLHv9i;1_{s>Ss}`%K2& zmi0}r0}9Hd8l_X7wPHXIR63JsYuEUXBZUKV+-Q1ajnJ)kxf|XYqQxh>HUU3lJA~1| z$J5eKytJQ~1$jvC5(e4SM7-iXg$|g?nQ0-u!l$4@{NzG*7|EkHwJ`OSo3*o|U-N^nBoov$W$L;n9x+EO6PT$h8r#MmQAakZcE~p#>RzN>C)^GW zOgEfaw1O*RS0pFHu89vo?L4MFP4MscyZ?gOe`kFL#diI$G#Y&+tCH~^(#_kCXx9m` zv8_von1DyE$?kjs&H&!a8oFlP2@l0gH@cw5HTJKg_H>cV{GZ4SI&l!_-oSt0G_J_eZCcf;YaM_=#*Ix+{Y8MjPFme+;1EaB#(EedXdAk89}6kGJbI| zZm>ir2|swpaGozt9OZw>S)oNCnfW}FJV>qEfGG}~&^Gc>EfX_+h{@%~$CIfg0drJz zA;#l_tD&W*A-m|hx;i?Oyc+u2-+#U9(iqc>MPyt&ODWQ7q|5wX2nshH{Wp4&P3pDa z>M=BwJpJ7*P=ow>TjECawkp&Nj=Knb!Qo+jk>vQtY5>!;L2ge_7~1?f++V0H_SE63 zIlBFsVqIlPL%&*Li1TmzyNT>?bG~?@K4ZzwBZn2s`qO)ia*~OJy?Y({8!E3oHWj}j zM2^i>$n#(3-rpCPg@v79sMm%T9;oBOhHpwk5nm+{O!8=H>*#qKHW|SG5J#NY(7P~A zOF#HLn5uZyU5jgjgM{^pJfrY+)g9FF89Wf+sU2Oe^^d49Cbzw+*%Zx@;*n=K$HlBl z-v}HN)JM)*n9`_dJ2X3s|4!gE79y_98pYX<+Cy{>!rlg*My$^?7lE$B-;dI>^S&Fh z+@&0sE)a!VDY8*NOq8ocEO z>p&(gdrr>{q zc<{MylzpQbD<~yDS{psev+02!GrD+TE0PK#cvBQ+gN@fwjFiFkSg)bbZL=-(Ox;!L zepVU%M@+WJc7U3lD8!gh`mHbd&7HP{zC^c1<1MAI{AI3M8tyBGk@{iP@_ELFojQ!U zNagK>s;Jt{KozgQx)&sDi*WKC(bNaBi4L^s`=^qz`nQlEDKV+tW>57zS5rh}11-3U zy{+dgR8C&+JwrK_Yax<&Aw@t$5~`QCRbBNK^?*K;5e(ML#->C3|#c_ z4b*E}Vy*d^h2nf8J;U49OQ7Z`@soYa;bjB1%mOu&H=mNugTQeUv)r9~D-yDCHe1`w zs}1x(iu0JB0_tVkbTr+(fOQOTA5X9C@+GS`dzzn?%pNYh;3~98HoY*C0Yej{d1#-F zmxt;dr^3@Ps*Glj1bC?_MwiPf0A?|Zu2fKr(nt0FX*3yQe=%9IKlwxi=g=Tku-;;+YbV4p0@$5J+vVY*UkT<$0A|lY{FgX)=DE3J zg&zzht=x#*C;xTD<)JD&pi~mr>oJ?Rg7UT#P?2_7cR{O&#B&)Tk&tB?XK@{V^k83w)3o6K zM5zUGSZ*!`o8aXcV~llCx8lR%w*Jrx5k>7K=n=);K5A^S=Tko2>}-%qo=GSY1Fer6 zcAFkaN4%+a4c=Kt+%!8TZ+H_g8oqswsYhWn`1RQfiQQr%+(%GYY1%-1R^=<95wU^R z>21V3cZ+v}S~Dnm#Bw6ZtvO}Rrh>@KQE`s)Gh#A{MD3*!g7SOR3KLGuuxE{mAE3aj5U1i z2&hZscQSe{FNQD!z*wh(uD7GF=lcfqzv*Z3TxSR*xZRl4>%N!vc~aJP&&s};>6(|t zU>~nBnGN%dtd-R?g7I!1oN%%;BS{S_Htf-tuE+O5!7_007Jy`*TZ`8o)IPt7t)v}` zh8@41RC`3A2o*#O{CE>bqb@px&4U-`EBc{iwL`vyF~7XM#0PMl$#;aqDl2xt7T0zw zIg4{&k)0EV(P6M^xIr`ME;qYUPgcR#I2;xz#`%-6H=<3yoM6K}_&Ti@fY-__X98EJ zHQPh+{Wxzk)Nv=1H9=5srC+`6dn_kCA=_Cj=dqf4eq!_H|LMpmfwr|}-!<}+d|Ai! zXt?g0t+hm4dF(vKR_!>?Y7#eOW)|<`_eAfOc{0c5)ysS_?y&q>RG6hG7{wqhfww=f z$q3^bg-b?{(w}0q&2Lf1#Tsc3=r?>a&)yyWa2}j-B5%I~#$A2Tj>dj*kotXG%h!i2 zer@?-ZP~)Uf=w=XT1x^imWnr_oRq7OY(d_gd=8^erEzAWmAz9Qb&C(kXPUGxf{0Clm9`GhlXmS{Kyu2S`lN4V2inS zbIhUAE=MvD|JU#GEJ<5+#%`31A_+HB7M1L+c3#T8SugKX-cShutBTQG!@zE4iJ#oG zlUiq$Nk&QzB&l2i>p_;4eD&o{DM>hCJd^AaG1NYaWFQ7=J$Kw23Ij__@>spyTEmX? zA@|ff>0GA)*%}+utXU*7H3*WaCq9IXai5{5opGsK^$dgnI_4-uqpzu_8&j_zE^0jd z@rx$Q%%9_z_>m7`c4UD*#RcJQP2&K_&kstJ!9HoAAHi`5V4Mp81Y_~QA6o~|q^08w z7J7BlRnh!VH$PsHN7_(f|oA7Q4ebhYkwi!$yyc z(Oqo#?h%u&k)OKqKX3#KOp?6FDC6!tyXhfcQ+s;cq6hv|^+ zE-97nE`dubE!_>$-Q6uncSv`4hjdAoG?EWoy54*E{^s-FJENj!_Sv!aUVE=Ib3rsY|BA)@kOA%-mAG}OiQMTR<5bTQB!V3SB+o|UP}i`1$#T9;AcYtkfAa>&S$C9-Q6G zCtJ;@I_G?yCR-0E`<+N)J>f~rCQ@gNw17-lp9YxV!xLrMNqxCS2nYt`I3 z*@E#Y>il=MFUi6BB<*t3k2B^u)p*WTRkjnF$s}zrf{OPPYZ>eC4DHr@(~8Q|rt=ba zvh^ktV?{hu>B9<{5znc-V^jhZ#Fn3rKmOMrCvi`85O)?m`k(5o$_aFLg7^;`PcU(7 z4xz#Zr)RHS+IK|!YWS0`@Q0<8gYrN--W;cFKdcS)pxZxvJekXU z80-S-KgK6L;+Ki;(t9|{+DG#}EBH1(uY8+rN@&VzAS=}UvcgrUE4SiOC=}IjEYlv) zKqAu~(U2k29@3yKvnsRVG&$Ja663vz4v*r{)~^gDhE$^{EaR3sHAnW)lc6O#ONM{p z92Q@?tSt@s>m})U&mPNrzSJmpbTes>RK)vCSS{gQCw#tCDoAeM;O;Z88nMLHM~jd=aHvf4GbLL|#j9l1J=y|uTV=PJ z9Y22FkdRLMaG+dK^wbD-V9z{A+hez<9NZBR&#p9CB#n)sHaj35FXw=Fp{t@ zf3o!cn;F6ccPRF@!;5|McEfdjRb&YdV07uZmMdvEP@$xQ6uI<_h>hIAz|&G~FC+8tMnu+rR1L2rwS@sS-rll*q_7CDWV#{dhr~z7HX?T= z;t#1R`wZiT6QlEAn2KEnubk(f-Re>Q)(@BwDGLVqcNpPlh1ON8Pp)(Ds{gphU5(sN zW)QcDY5g&tCd=-(krJf;T|_QkXd4XmAKWiE5+sG?u(2FdsP<})>MD0_0V*6y4DO!7ECTx@ojE=9I|%m>Z~j7Jq9aHZI~HcHMswycf|(1>A2~Su{rtex*oBzjx$&zru~8vWk%^k@ zI_hk^XA_0!?K2Cz9Hi%Q$)-g2V&VkH>Ko_6wIIB3g`=&tTII7ZxGq^#ujClw!}quD z;f+k#J}IESe9H0k5J{n_x{DbouoT^B->_sKOkX&y6LXyprixs{gv*#FVuyEx6ls$x`}aE_uv^`!7a)wF6{x-4jQ zP3rG$ACIF!p`0JcLv1Y)q4?Jl478>?(m~a1bNqz50Q(fPs6%;&KB(8PTwS!PAch<& z2*FJD?(pN-|JU$fU~UHW`r=NIW6Euv6X}+rm0rKS{`;x1b$gx9@APBS>%_hF!BgW= zeA;jQ)l`&~WAYQk9>fS-3CALouy!xzonzuQt-7|&inI4hI7h;(%NMqSd+7Ssh2K1O z2#G$f`4pd+WyyjB8T{5I(k_rz1e(uB=UEHu$F`~)am_XVF&m#yGg6Vuwemx2U(Fqutl~2F7UPDH+DP*v+J#sS`XYjooVb3Dm27L!fB=nau6O@H6xAcKCA3 zs)-v|ODiha@d>w21LK?MfVQM8U#2kg3QX*}1!9ySq5+J$8I0B`a@g_h3w z1b*8tyV~lFdclT#?+`m$8Bzk|%04-cAV#Om{kcm62UGNj9fNDyucYb`-YD_i_+{@N zQ!GnNH2(-{@C2mClZ$l`Y9G~=tY6KliZqKzbC(|swsg{ z?)|AQF^vB&1aI%u7-ff99Z?4UK)bS9v-yT`)|T=VG;90*6o1y1)`s44Y9=}95v9?p zALaK3elIq(^bG<9?=p#ZORsaOC5TC{<47|)Ce6L**U#TJ$dkLFh4Qw`|6q6IZ3U<( z{$P{dlRGoXEGA@HD&D0W$X^ww^i`ootn~$xRD@-=Q|e-TBVRw}64dXLX-_t8MbRD0 z>#9Zw5h=S?Q`}rcV_>w7w(ykvC1HiBj$!H@RViVG^@HcTOd)d}C${j4CJfOCWeYiW zDKv@+W&g*0`>*wN#zzPw=KZ{XzYi>TvC@|yf1E`fX%3LvSGJ6a_lAjzN~g*UVZ?Hb z3+X{3mfPk+pt)VLC4|>ZtidGs5Nkcnd3<{GE%_>cq9VG1?i1CuhCuiss-K0 zXokJXa~&uTrf+le#wfoLaPfy|?aN1ov{FCuIW2j%N1&>AR&tD~?0sqJJr+sPNb3)E zM0TvN`s#r57sD_%gWGf}%2Ec?jh5N4DQ}L+9L9M&C=1);;5FW@V#R&)SVi=Z;o!YL*G{V=7Gw{OK^aiCK#;W!-kU z&1RMrp(aWtn#6Itf$?|os1@_`hCifT1ZXMSeOEXX5>T~Y)O+L}Z241VuhG0d8l@A7 za`@nV{3Rpr6~0iAO_5PCV#aNYpaSC7Q862Vv7h@smO=tciDMJP#$#K7Z1B~Iq{YGb zbcQs~xt5*21}U#pkup~?ubOc7;M*PkeR61W$b!0sZ6>|j44nP2w!b3QtZaN7&F_%w z>=#*F|B4o_cw~aTKh!?(m(?^S2(vpdE69(lT)TEs55$GA+Bl<|TbsFaoXsBhR6X3Z zMPtMJ#~wwrGqv|vDGZ<*w8-O$N+z4aQ_54T*(%yo{@HBFl^89xh;GqxRzZKOPtbL6 zzY!-zpd;vVYbvUUP}nj92Gd|BRSkJzZ$7~~DlztHe0DPp;^0M-->*B@%#)|i-L71aB>Hv`5F`q z2=R5FBGe_Z;R65D#36t5Cg8+EOIPz0Br`>#F`JCyq}zvvi3oy%;x`@~rgUQ#qcqK; z5~XCi8y;5KGbvDmCQ~*bMj-oRCR0|3%tPp`s+qgP)6ZG=(j$8N!55S31{ExxJqCK} z^l~n&71&ti<*aM&cA%!|>n1I3-DTbR^6+k>G{Du$Ad5Ix1@^3A<)6vcm&K*;g0HRE z?WNf5Y1r*z5$kvAiyE>$lEQ`|E;rme%R$RbQSW@dM*KU5*0}60x4XN1$?kgZtL8MN z`KukeuqB|>KjMVs?3|qN_Y_+aBhr$8)HivCuW9{0UU~mSH(6&&J@ZKm%iHgE%>Jdx zq1-DPYgZGJorTT|2=V59oO+@xPNh6v`?xu9^zDkta`h2(^T#ytk7!N=wQzJItD9@M z3E7XOs-XVTvut6(uqT0~YEkp08Lp*OH+Y|Yvri?G#VILW4DkYL0tpf0Hec}PsOF$7 zS-3?7Eu-~`@&`(U<=C@aIkh%n9#lT_hL5YM+d&KP3W-17ip#)tmJ`%g=eze#J_kLo zKHQ#>&>AePV4pulP>oc6juze3CKo}k{UWX&KMQkV>2OYCCOZD5`<~bBms9L`EXm!^ z#2Cd-9)3libbc8t9K=vOdY58f3Eq)O#ybj+U{_pGH4#N;^snze9j6U8^n982rV&;&ro=)>%8bz=maP11A@ES*x_I`N|>ckgEQiy2Nj!q|+ z(!PnJOhFi4N9p|A&#tPs#v?e&GWj`oxq0wU=eW800}`6;#I z2#?s(?Bbk|DAH~QS6BUG=*?{mx_-p4xzE8)ki(8l5XSMvAh#p^=zhQp?A^t0uTl5S zrJ-k;Cj)vd0xaQG^y4o1`GIDg%Rzo_w$&IM3X;dBJLIEqL*_?xhi@nRtf*Y@zct0W zBVmoRv@Ta`i}b!Pnmz~ZM)-G!uOh2W!0|EYE zs!hX(XiqP*<>oLEQ%9H<@&O_)%r+Z>* zkDRg6q&)1ca+>ARvZTJ+`ReD89RowpynZyFxh7G99cAKs@H9-Sd*l-@>+;V+h^y2@ zO5s61RgofNI@NgSF41^fKgpbSUv;Dn!SbAO_*=T7cc*bGDD3Zvq}bm9Y@3D>4z1ws zoW$%IPmdLP_nT>bHyw(bRZP4jpK+W6{BQwuG?xk`5A+|WC6e^~jYd&>0U9b_$@?Ts z)_m(^ok(1sG@{qaj(W(lzT*xjE!k~3BTZ79iF9Vp(dN`EnNKJjT*d#}=anid=jTuJ z9M$}kGcdV~4I5`hq(1Kv)OaS^c==xfdA4&K=`Ax9dtw~>S!y65EURk%RZZlh;~HNt zICz8i-3Xh9eS=*vlKlo)bCgHcDbt=^IyYS*Q?cJeOtu++Z^+KebC5o(e~HFzref~r zeK-RHW1*MyIA01*cEu0pBPFac`z@zV9+@9S$69#?;o@<6H^&Nm7fl6Q-q_2wT506n zO2e(}y%Uz(JLE+N30H!Xj!d)aRQyB83L*zSVy%h8?7DrUjT6<=Ys_?ebPVJ#t;mYF zJ6m5`6E&~o_6=vT%!R)gX;%L!+1aPo9Z?$AoUjWXJ)-_awF4*6i^S76{rBg}S%q$= zbIwk1#@2qJpx2^{HfH(mcX@Vpi?$KNHva{W4SNyWi0-(wv=)vgF>u6~|vmuWr7p z%Z^L4J#ZN2v(znw$&;k_@HWk;(aFJoN1i+xcA~()he}|lhZ8t^2zSMQFD|$-aC)97 z08O07dv;OcaIffRm$3ddX5wsUQrG;J=c~-Zbin?{3vE6Bim$(yFBDJqwahLRPnw*K zZKV||8imMN{;fFM*SO_Q!q(z)pXJ1_^Y-$7=BeckDN0#; zFX3?hdf5H>6{qbh#(GuQ*^EQq%D+ufo3AX6;*L*Hj)(~fVSes%I+QBxg_nQVhzRKGa@^)_y)9U7lj&X{w0;F0~Gu#&K_F97Sbq* zWN*-ib4OfnrqOm7rWS0bC%fHOzYiDPC6}+1J5`~9>Xsu{{1>x&Xq)=gd6(9ls_snQ zB5<#ZmL+$dcOB3#cJuKLiJ_Nv2oFp(&j@HtEo$PX&Aqp)hOcBbuV3$cQ$l6WL(gxa zUP%le_3pYcUN+%g?Ls#_P@cN+cDvA;&cl4|!lvAMy4?D@+{8BBg2rz}UvK+fZY@hL zpD7Pua~BZ}AH^?T4ewr&z>gnJyr8=LP!B1>rWByg>Hl7-OyACl`e8PSz^>-NcTM#k z9EiOTY`JwIJ$A$8=!g1XKyRuWJeE`LSc|^$pFAQ&A7@bRXkR?9Uc8dsy)r-fVLS+; z3T`59P(VG0)%XxTkVCI1-3g=Dkq4Yg89uVzO&DL?Q{O$Yc?+YzDnLEcU)-17E=1L!(Kr65eJ6;{Uud`~ zmwSG0@OayEg?RTwee#Io{X%5&1n>UxZu1o($1m*F&ma9Y_|&iW6?uzdPU$~2{&!tp zrA*b2OTgC`&&1)5f`tWT$1blnZJ}#!JvX}iT42y!hx)Dm<5JP{=f@(~O5xCv6cRAo z`d%B>OMuB*;W5v#-L&0|-Ji;8x5#R@sOsCJ6_k~z^ls4O&;13zO2d@S8$oZ;XCx%~ zHK^`@Fygsl%#8C-p6 z@by{C65jT0^exm@pl}gCMG`8?I7@1uo916%NqwvZQ`^oZUicf|X*?+1D*aucI0pwL zKC*S8Ec$Uh?;v-BZYZ2G0wnyw4_JnV*A|x56j!>jdU5R&9TMb6*Kmf0Y_ScC3U|y> z)%5FTV)+oekA?yMP^6}!UP6Oze4aw5+)>z)g^N>2D5_qVo!NieNjtM$GH4^PXO9np z;bEvGs}F#;^;;%9B0LHgS&&i6W97J>@HY1judSPjWP_!v50Fs?s3SK$1I``6o-;Pz zXg*0-Z(=5osumqyYj=&!9Jj1y=AjNU?6_lAjKKz%kTJ(xUNvPJ=jnh+3_XnUB}=qh(ZFR0BVd0D;|_TX}0!!4HTv zjBLlsF4qyXCu?`~n|PtRP4&0>woaBtACFuDk}ZqB;YBBm=FUZARL2{j)~8@|XNY65yFO8b^Bg54~orWQ|XBp6inkgx3vI z|C7iBww(w@s7_u+0!P;#eRb50;@}4uAhu-x zTH2C~r%yypW(vOo_<uMh3zF> zJ&ZJFtz3=O84DmR!ajcXAkiEeFdr8{k35BW6JJ?9ha)dY*lkN5j2lyu_*PqG*MjrWc6$^(LSsXo^c!ub;@(J8t= zu3c=cZS|653_sUROxTIar$*dYZWXN{u??I4cu!}OTKXUnX>ob>gKhIN?!L^l>09{nJ~8Rz3qv?E2b&*G$0YT&8&E@-;aR!D3H8cT z;(zQHb~zb-ziCVr>4A7eSJQGh1H1?OpCEP~S;HsQ3SbpF3K$ zA+Mf61IWe%E|yMovddsOG0=?#tL)<64R2?Ldj z0L~~dJHFZoe}JS#ahZzv4+k-vo5HLE)naKXBW{Fd&)*LhNFEWc`l{nT6@tF^n(>Rd z&$dVecaA~FNzqc(1U;m0`zk<4Z!HKA9*U4^gm>7&j&b@WGN#z|Zmdj1qD1HuljhE^ z-mJtRi4CluM?uQGqK)HI&LN~)@lN}X9X3ukBMfLb!<(5J(C~C10rM^UKxQ49WbH}T;$`48->7SOn%jP9kGaQ6U(M| zEFCd-F2ycGG#>}_+C%4p|6m^(7_f-pj(lMU_?dl~p|Njj%Ln_#fYloif=upNPFEZN z$V0|@nmH{$EUp-L8qWreY<-y%lf;A&fq*P;S$%|L)qR=zs+M!dl0Q*R#Nx%&I5lwv z_(FN>;|IVO2@tVz`04y%zz@eJmO~H4CSq7ui|!*vYRVgjvfenfFtbX%$4S1_cfCRPf+<*N;{}N~6 z6#gIkfj`-%4n4q{AjoQQ3fgk9assHkw#zl#@Gy`H0=K3)a8TDikGD$FeGGd-(@;!B zX;ZTon95p(48WmlwFjPsGv1#=^r4C9z8j)86}pe&?IO zurYd4Tx}^1+HT|y6dei>D=!>;-2K1}#+EE6Fn(Ggl4tp12Qg<`x)qok;%oVa;lwws zP>0xVIIvG=S^$pck95bcPKf)!jXf@X4C2mgE3oa5O(#lJ`H2v_Oxnq$K-5QXs? zzAwSS3}m-C-iZXjhqUG@yt6&<0Dpv}_Rz2>2uDnoPb@Ujl4ktdm)r*g0pqnE0OKKZ z%Qagn2hz@57Wau`(SqX=kiN;ktK%0hZqL=vp?TPt@!pf-QAw z2h2pbF;FB0FcU8j`ii|?rn~ur+!SNJvf3X1BOs2Udf@Moj?68`M1oXMjRP5rDgcZ{ z>Z`BQhhNKPxS}&!u}&3MtWAHgFNXmwa?S#9JqBVTm~6lkuS;^ZB{*;tH|=NsP?>;>XhG623!jCfO9fI($5+JrnPzO@vM&7+WpZ-xRf zAK@V*)~O3HBw*Z2tiY=jbZ)Yi9Huxv(+*iH4Q_8>2mu{GhbV&FTdFJJ=|kbNnMdSi zJOs1u1z{jxiUELcVQ@U?;$xb+IyxAXQmq+ZA?Vx?$ z@Ohd5s4VJj(td2YT7gZ0p+!owgOAye<$8FxW7g|=!}m}Iq=E)G(Sa^tLW?kZg-V3XrQbt&ngJnVu}wf?~qvkfmS{1P96KsW*N{1NBuv-gzcX zJBNTO$>kaApzW&UK>qw0$UNmO^PfQG-M}CoB-}%$XI-^`0K!UFn*BpbAakBKt4e^C z0KsLcuY_`Eyh=bUY_?-0O#6p!0Cmr6fq;m;r|+W^pc0r7L9gZ9mLhxr%U#H8QCZzZx(_$WY$O%}NR zgjCQuY2HP_=?OvA3c~V+U{E!%1FhdU0!huvSsLQ4>MD%_Dn?k8t1a81Uh#`SMJqsk z+qW6}61ZAAGXa?#=cuVCN(-=9*$fF`vndIdlXE%5|N7fdufjL06bJwV@-^q4YWh%I zwV)jWMIqke$~wDDtHfC^;EDtuavqS{)FHmlAQ2H_gyp$Badw&*PmE##J(kxA5A@Lv z4_H1Kfb(+>5g1~hU@Nfekd35TMAQu##GA!t1Q62=8A3};uq`l#aZ^tpEWk=-GhRf^ zrc?Nd&%yvLAK$Qr29`BN5~OO3`*B3k0>ly!h1pv2t&#;83)o#BfF(ta3EUbwfLXU` z6tFSEAg%|vou-pzqdJj~5M*C)0V`pqmMp1LG2pG}sV80wutV95IT4Sxzi~H(4`ip( z?;gB`gzx>rg`F}&0IVN5;VYcn8G6XMfy`oicxpiy!1C(1c??~Ku}I0 zu}_eLwjN~Ad{lrz(|#MFrZRz0Lu&z zWny7I`^91f3Mw(NthP;}6TakRKV?S$QoLDFL$)lr5O7rEAZUmcAeMrLk(wv4srC;6 zRkjlMRzSnX$gb0G5buOBjC)s3?@X?=PNY%|21d-&-? z6bN0|ZI69vCJRJ<)9hWm5&>p@SZkA4ht+@?PtLvM^kI~0K{td|e{^W5`66>ZVE;M%B;pfNb0W1}(g|s5qR-n&)MEj9PYg+|ZNbi$i1UH{&mrbc z(>6*72qNGlr?1~`fSA1iSDO+*p34l301$>!jIG%kekiWgDLxR9=K6SYK>z@D4qVp%r^+%mp<=qkxE?40dTyiNQ>7KIEeXF}%{Zht;wb@L>#_62;e4R` zb7&mB`uXMaH1N@Ri1RkJg}@U1KV7!@gl_?6Dw_$4UX7f=Hoq~4sDsbGZ+avL2Xb<# z4zGaR;Jfc_rVm$D3s~4~8LHIBXHo#Fu)G~aDqurak3fJ{f8KW!EjU+<=^AQbdQB%> zS%h5u2D>*xWdMZwKVkMbaUKZTEF$&^bI>+#YI*2}=oYzMFCaBf4SSWC;M{Xy;bT1Q z-$J}8#rQ(>CrW#2K|jC(EC4k!U=I-k>r(1zzdv`TU$sCALiQS)ZVDkfKm_#zgo=Bk z9V0*}NXQ`2W-+l)Zwr$*4c#L>P!tW&Oy5rsEht0_F;aM(pzsHYOBFwVX@vyl*&t z$ol7yCwf&Y=RM~`C8U-AlV)6=GJwiE;ED4GNtJ-Q&DR#vm||BbfOvjyt33x$yU&`y zIoalk3j|4(g8WR^lJzCVXEp)k|EJ6Nq+Wsj>32WjFl~`(~VND-m0m5vxZUHH#3J@~lZv^=Y5JV3k2vRB}WSnx0t;rgG8J^TB z9YBzYHzP?vNs#~qS@6Fnv;Y(SIfM*#*7EkUU%R|A`Lf$vv%dmcHt_6Cmcj0`O&=1z z$uiCN!MQxZDgB=+(3!m^lIPowxB2EexbjvzEQFpSh03MuifCr zDS|*Ezlb{_m-RR$1oWo+{-?@HXS{$4eW0%#rVk^fbMw{Q54v~DFFt8 z0y$N|AEW^Jwi#_<5~<{kBZqtt{(U8UJ0;95SqC$~+Br;wzM{{aQ78fB7_B82gLIyO zRVVpK`DRA_CZ5fKm`>1FGSi1LWitmv&3HWT;e3E2-4zbZ1Pd}m)#w9KBn)O>L98H0 zIi{r+4DSyzEVvO&2OQjC!c8L(=IQ;g7dHW)Ptm%TALO^YC8VI8h3(0YL zMmlISXijsYpVX zsS0VodN^#6;cF~apAj^t`&I5&lS8MFQxjhLL6vx|-h8L(E?bCoi=0KM4jwTy3>4-W ziePan$2rZ@;^wTM^yh5hv0!j@gq*#@=>Y*9jHbSl7;|-o_WX!}x_fXH!e zeeHt+*pxmA;3pAKJTPR>832e-*ru3MrolyPjb4K+<^>rl}j&q#=X@8FZ7yq6s%V#bgUnh_= zh-UbfirQb$Lpo^uE<~C|%!2$fqzon<`c-UK-yi(RPdX?@u#g4x@B==c!yA6q8cQpL z!Sk4{a5!NjI#T<|#x{5*-WR_LwyHUIi|&;UHqu9&+b-pdh=Vhyo6s>dCFH&FUK@+* z0{^!S|I!lObB%gWCd`Ss6Tc7LqKa@TG@PwcF3mGv$6@)#Uxamf&O zArU;dE`ASg+d%k=UiIg5mfbpRNSDL5y)-5|hW?i@de1$%Gw|mpQ63$Xzj7%Ix-NGk z!c4kosQgto>j^7?O^_P zx=gR1A`@p9J0D`ecOUBrWvAD6kh6G;U7VK_q1@d4mD_6h7AqH^ZgD-_C`qL)5WHoM5tE=$Nne>%~qx=aU;}hYSS+f3lw50r3%AWVT!D(-EGO@?aKiRl6Ly zLu;(H%h2e~in79P3;qqasF*!--Ed+o?;fp?2I*mn9#=N!kBzfgXIT8`$f~|TlGd%U znk`SbtJ-k_p(>`^;khrLYgou=10#OOP~amM$PfS6ppktLGb0^C#Z@y*n~?EiHy~y; zm*PLL%nA)X`rRYm6OT&cW2%tJn_w&behDXq?gTUuqsU~HTKTX>6&+*l_y%T z->H*i--GPylkf~_zo7V!C&$IM9jzdQF9luDW^0{TbgN65!clnq`gX}u`4NsEoV$9{ z$px<$l5WY@Oa6Y`(3sF`%yaYvuaQJ%{R4uzFSMYi4qHr0Q!`_4H zsW%TvFZU!%Eq!W+bP}pz_t{4V6ss0QY~sfhoprx$1t=u9H7yW@wz)F;$x?pjL`3rP zxb5lfI)c$8>YXHXj^Tb;6<7C0SkARxqdf58B#tpYeD_7WUU^$1v})#euJx+Kv`bg< zCskd&Q*8-<>n|k>&2ZxC-TqZ445EvoF5eEdBEax3A;h1wG=5@WHoLQJsdXac6-n#I zf-z^+^S8shq2sM!he`-%L5jKkn{3d^Ow6Su40ArTgXKe`eQ>4R|Gv~(WHw9`+R)mN zSgxuY|0BNiscfoQ%BlW|Q>aB1Z$&Fsc7?30OBw&KZ!SPpDD;5+oobx5DPe3VD=E{k zeM4Z+jOZu(LJsM_7J8rJjNZeDp)ew~VH!u_K~WHuFwo>yji9e&!v$mJ<)RT~uX5f^ zfKXSW$zfmZsST*n(mjO`pytRBg`&+Ad*{+^jVPI{l0n~SWT6WO<>jzo#zhnM|C<+t zqCUdMInDq(CquVc4vq|Ks|GY$_m7y9La8g6BPIUOgJRK(t{BZm_`{#MRW!=@6_4jH zLQqNwzPkry`!ciaxa(uP)MMylJ0ghqH-k{>Ne`%}c9g-0Xq>fSq5Y3rV`a6MfkidL zbfZTuMTOpK=nZod38wwuw$OyGzhjSHJqBVL{#i+ND(}|ddlhH6kbbTpcs;;e_z;}B zu77N}myDR0BHkxnW&JzJVJeVuo3HyA8Y&fa%E9ir7tocU6JvERWB~fNJXen~PU-{e zsm=mZF!hqxi$y^TzGaVnd0zK=Z1WV#f)!sc_`cbjRXQ)dX+dr()KXsTl^d79oFbiq zqr5tg@$76UCF~)1X?TB#A06V8XKbkLjhi$C663l1u!K(pAH>c_SRWnq98^Hg&W8d5 zg<;%8FEGX|6h1s6cv>e9S%puh7yVZ=$N#Sdm-rAbc!@1?CGL8_T!e7$uCcDvt5D(m zT=L|?!;N$;=Y$0GeO!-UYhdW8X!y6BpZyGtr;GfKz4G0__i!BT!w%=NUnn-IRJQ!1 zN*9g`oS}_G`V!6ybt}haJ6{uj>m9VroirU;7-q(PGc)DP|3bJXVf$&g9t?qGUYs27-`vI%TB_~A-o z*_`N#XzBLjMsQY7bVawYGrl3kVvM0TQ{5cyi9|=PSC)W=!jt^oo**8hAl}v^5H($xbUVvD2BaNm9P5+%)D9~yjKz@O?+C=qUGwsm`*gPhdKp-9%S zqHQzFDc5(`)KX*l-SCQxCv^vpcn=ZwwfnEE$(*fw+y1m@#acINtvcA$ZaC;@Z|emy z`5LTg{nLyCewImP)MnXQw{i7{vyopKACquTkrMkbuG?CDWWtW9h~$(Yj<a9GfEx zQxWTf*%9Kz=S|kL*@=k7$>s`E@!%$Bjj3#88|?WUN?R$A`H50zOHbGt(_lrD`9x=$bWL6Scoj?$V{L8Sxo!a*QlTD`d4_DzTEJ62QHsDm07oz7BRJ3^Kqy7FiS07){R%~4Qie`4K#jt zV?YeiZq07_+3SXyy$9OXiOt_m?IwzLfzn*0BqKEX(6Z6(3uC;XH(Z|nH_C|7a6 z#sP=ml)+fq-ZymRW=8}Cc8%Y$&ig8Jx?*KL1Fz#nGN|q$$$u$3j5V%@yTY|mfB8r=_f*8zj8EHXcxvPThOLNv&Cwj;k4puZ^lI-Ie z#7;aCy{?vAiBW6b|GF|uKQJAn%OiKY(w9pMPT;Sp|cywE$R~2;`f!P z#}$v(lDMI9(~v{QYe5^>FuCmLp{NApf!o0RCqqoVBH6+y?G6#Ejgu(oyR0iodKmR; zo0`bBYiI^)a1I|j6C%YW*698X(b}p*6>hm+)A^MCn7qGG$@m_tcIFD-Y_F6$qHXO| zf!-hOysSj-TCtFRrF@lcAEcxgW18`#kOVury2{hgZRdO^JH|=W-eTw8a2^>`P|*MU zJE*{in)I*aqPcr>(SOQ1@utP9@arz`CL%eL;u%ae@$iba&McP+n=^NzoQCMXrfk`7 zj`Rj7VbuDm3B<@>f+t}MBH58@dR%S;u~7V+(PBfDVb`f2iP2+kFrq8YR$%_q)eS;K zkA0Y2Z7o0UtI87S5OCG2@!8?t3?XsL>~t9pnaCcPtRf?LU|xHOoDgu+t0}rNDW*cq zSa6Qf-&EGV>tv-(d_CA_4@`a=?v~2iqKwHV=PtN`RT}Br?o3N~?W6q{ZynsE{&W5k%tDZBTmh`?I zn62895-00vB4X(kb}PZnUxNkGqxNIAWya7(7R^y&iHH(j6~opX7WO} z&&EjA{sti?P7tTY5@w-aP=B+UhPzI7&J+tW;}EH%Ou(4V-Loh&*wtvemM*jJsF&8x zwhj8&NTR-W^e1+hIrvH(znyMvYuJ|P374y?Lx3f0m^q3VFXeRq?AfP({Yi6mG4tBD zr{Ap7Dd#5Jrg;mk`00KE;S>3$bM8%;YZdgJt-a9RKUl_pRKh*G)4jbLU&Phz>2v<6 zGX9D#Ju|#uA>*AEX<3}QLDjYWPC~q9&4+D}gyt%%`RXl@T@87yS{g*ZB82EX`C5c3(MZC_hP=s$fD06dW5_DUXJn^GdMgw-6TsmW0l)^WvbzB_LW|m@{`1VIf$Em%#h+dKm z$&=>j84g~`Uk3lqc5_c9#zZhs_7Gar^8av59+8K0$0o4C zivUM&#^GwT{i_jPfNhm3oB^GA(w*VN+7{;A%!vc?Gt7>u7Jq;HyQK}65z_<{NUoxh ziC)eu&v;Ta=9g>qP-@Ctm^wYlmZ74H6ACya2uEibIiS`CR=KyO@iO*GjtXV@+AA3y(6^u- zGZ9#pLv&9IMOsvn6JFu!Z)T+SgSyBb*rRy0Oua9zKMQ5uhiAdW%u#NcDSC3Q0oIA< z?*cHe$Ld@d6>{K%W`piS51MTW=Me-;YEZ@nu2<+Tvj1sQc5SSC=x7^e!XnTsz@UEJ zuo-Stvp{5|yU0R}LVhA3T}+1|AYaVyjhZ9j7}xQ{acG=etYNsvWISGy??{+PNS3xP zJheE0d@|TlRedwiOx1Ef+^PkCBN_YW&^Ie1SeXf5bF<8y&1KNv=Qv3f`^^C6;3}`y zyXKF}a!XH*L<{yn#5*0<<*+K~tA$qXUoKmggZWKZ^kvsxA3E2EoblbY*l>7x<>Gp@ zparYLn9u(!a$8AV2r9;ih>GMtRDn>I)&8H8!n@G)#%HHf9%X zVHV8%)ckC~>XQ&-slqOu%FJDUQl;CmLPI343O`ilc{!UxbJY@bE9T9tFN^V5G~8Iv zWidH30WM!U?z63@`yoBwa~z0_El}Q2TV{Nuu7YT1b6~`}dzy#70g+Bz^S#n zZXcJWm2+%Uzk?NXY=xE&2XT0_wLsht7ZNeW$%!$z*>V5PzpJYy!0-}m{PF!HXl~)9 z3|m?s!K}0wFxOjZ1FfhY217&H{n1zI2xH-C$B&9^2;Q^) zNf+Gj?kbi?u1`On?kagroo|uiM9DTiVWV|1ZF-u7Q?wlScL{XV}!;jpFcz2*Rtw%e2zVxnfbEs8jNn{ zT-~Hl!mj)u*_Ofx{F|DR2NQEL6Ys(V_(|mlt zqtfauE^r+ceUNM`-{K5=(tyLVUNY9@9Au_b%iQ#vV(>k#?NE=xI2TEC%89jNC6)rZ z?^Y?N*OnA=tcso897ep?eNYYur-rTLhhT-WHVe7OXz{!>q_hmSa0e~e12cPE7M$E* z1%47mtNkb)<0br+dDLS8ettSc*+lqRmj(s6q|uABO*k!vN$QX{Wdp;z#p&od zb}Kr-#1cV)XawJLyy(`c=wS9SYZUB@*XnPM-(LvT`YiL~?=QdeE~Q8{#@$Szvur6x zKDOXb=M%+i?c>fWaII0pMZnqv+qgV5OLa|8+xHJj}`w5nRC}pIu6|8yj!F!<@X1GI7QgiQ$>= z6xX{$Xwqfd%vi33#D5AkX5}QPC2(Zp$?i-F+ix|06z~nL-09FAootG>J6q1zXP>59 zI9U#ku6^-qD(Zd+@W+Zq?Dca6MSz5y+GMY3k4sN=>vkB*nV3Lx-Q6LuF_|-u=fZ^JHx?Tf%e#fxMwF^ssS$=x| z(_^Obk8TGup`0t4b#U%gDC$Cn4*Hz)f8yl2lV9*sfB#wQxXW*Z(ISS|n_$W@*DLG$ z|A_j=;7pic>zEtcwr$(CZQI6%8z)a}+jg?CZQJ&@?|19gtvWT+eP(KEe)ZJp?y6bD za6NET)k-#IOY!$!7@K5fi^&d-S)eE;8-~EhniQny;cTec&WWYrO2U)tNUUDkxVHk* zU}n$hKR?q{rQNr0sIv5|cCP}xS8W|-PGw1+&YB%c+P{u$URAe9!d*1$0q+6K?kn=> zXJ0Mc6J|FxaBqwY!dW@{5*_BogK)zPAYXDHhFo$^I5yz^zy!#O@RvbDLVp||a65V_ zYKK%;z=P%gNm18q0=s_%kAl&NONr2{fzndD(ktd*WfPaep|FFs(vjK_UtiKTBdUte zvB0e3@A$RM9B~U`(T&UC=&rE~J{|5CRV8n0;ID;4=^SrZcw}CFoUr61jwtqV7kevujOEl~Aa>csE^#0|1wYxYBOQM!= z{js3Wt)~0Ksh^-S1`<+1sQ)xv<)U8ny8%CnOCKw8B(I}AStm*y`*ODWa?Fb zsb0=5=x_^qupR4~Vl8DsRi!-4c5?_`$XVHB*Kb+OMNkWFKLDrCs14mKi4oJ`qQ%mm zdzz%>HXQsrgGpm(+R$ZKREEraL^r=C$+72JJ|JUgVold6oc>Q)?qfy&Nc3?=i=OQH z!~{|9?PNN*{Ka@=2KK;2e}j<@3RC?_O?kJM^2A7y46*a$<`ms;&f%80b}}Rv8R7=o z->(+WFvyEdgN--bxEieejCAkv`zqv+!H;<16E3E38G_s8k2-TNoHdywRWeI(_&f5w zOWRcr?Yj3xg*mQ(QoKbqsAuR8$zlV5CESM0& zK*0_2Nd)OEjG;CaBAhHidA<|QLymHu6>^vZVGj_&cT~%Z%lag} zEoIEkzjveL@cWy8ogm)*#;&`}47YAPM$GsPgJLb5prWO}&q|7#5fbQv$cPvGNrjS@ z24#kDnTrjEDr3yD22$#N5}T{6ljkdt{R>3C3De!2Fwcdo${8o;n|n2l1uW*|(u=p) z=_^S{u;Ckx`6b60inGw^OQowu;bebBcDcVCe(wkf#%@C2?1B(7Y^wg%G|zEftag<% z9h5Q)_9FLxA>cCbPB4SNEWl>V>!ta#h%Z>mUKL=yiYd-4bF5^c*&7^NO0e|fJThV- ztNLSViJ8k)_m_=*!P3*b;Jwr{>^RWbg4geUGH>G)mHK=-JdNC^3Gby?*8F~mB5CT7 zs^ERCV#!ixqKJGZEBGDFl}AWXfW9(%V)4IXlF%IYvzmxD5Ooe_m$M#_*Zm*yiekYd zZm>Jg1=+02oAkWI>+;ELU+lh1UDKb?gfDZo@XaxU3Hy)2_9P}XffR3#s%>3| z0BSeKA_t$BpqlLleBDRuneCwbgoN+@o&=5u7>O;8QN!ri>>ODAkxSJb%LLX10 zPS7pBuR@JPy%g0`M+57;TF5liHD5-lfSP1warnn4T-;N9^3979ANxbwX%|{i)0D#^ zt2`UntOdcB=a&X=_ZF)>)DPBKK9OguyiRfegmglQ5xEAdDE$o%evntIIxA-li*kNk zuAA5M@AUVAaN*G+A+L_W^s+p=sQb$#74H6~Lm8Ar$T_HGKSGf?zK}$8BWV6C|{9bb-s;_H;3p)*(-+bCx)) z)peiDFlS-~^_=#ML~+ngWu_AQd<9cVx4NK(bGQzbwD%n6nJu>4((8>sLivLhBEbKKxb;gjZtPet&aI z>DYtoP)(q#I-vd_;88F4r{eq@A`%q%c@^8>Te`vyL504!SMQhhnsL61ag46S31e35 z!LDpL&hOS^d9AKSK7ZNJ7_xy<$-m(1&e8}+FNn=oE)L_sP3r+`QuU{L-Ud7OyQ1t2 zSB@SP7f$QFDO#-7vljuK?{m4MRiNER&C_{;{}!4~qFOlb#;iV- znH_GZ@Tw3@t4}yYc(_!yb#qfQ_|yMXOR6GSAFpt5|7o7(#+-hbC0<3g+=?-2R0sa{ zB%2NgaOtUYLhv)w`zq|Es*xr(ffm3LcyjKFKEK;inF&gvpSwJA)lC zp!bQB#cYnC7wpDp9*hj@vTPr(YHhScJy{DlB+hQ)@)YNmw2MvhN~L`iR=uW9;>s-@*@9nie-YHmTX$0yfCFEu+x*#7n_8A?pKk(# zmw5j8Lwix3ih6Z^bz$yFz&dbqb>|#-BX73bJv}AW?H}OdM z;9Jd5T!9yDiLJv*`5;*_kpHi;*H*D93l>|QfW9{XPRAn@@I4KD+8cx;B;fy5CJ#rD zgM7aicD`5_mdDNJ`>Kw7NI>*AASL({NA*xZOUwG7L3Qt-3fz?o>{(9B27c>R&Kvjn zfECCfiFT`p+lNsc^*SNQMB@a9_g0h0O6Q~ppg!t6u18)`oJya7Dc<0^@5LecS<>7p zu37Swd(rMH`JgT1z6yU|n1Cx2=pF!lD%J4L|8@lv`Z`nNFB90>-AUC?f_|$d``bG& z{Cxp+A1iL0;92x$dEyJyV}YcJK+A{_As#PeY}*Br-Ov<=k_rb_3kv(|W>ynl@F$kN zggx+E6EV37f)-vERv8u^FSP5Rse~MM&wwcmOO(VqA8iVmaB0O)y{*%&b}l z3gv@u=v-T+hAr@m31hK29C`lMhjB1sL^^+#wc19meHr9+R9A5YTUrtngPzFGD?WFr zxkUB?=+mUc?pPS|)!-CI2kNM#m8b8=>+x1F`$i6%HS;dbww@(*!4WJ3O8-O8s zp<5P9$D0bFdPCvZjov4a_fBX_dp(2`c+NCrAk2Y!V8ofKP*p>*XMA<2>-))Vs68%Kg83Zz%k*8@z^zHg zvu-Q$_OspP{gT=<@c|JIf631{%Lf1%e7H{NF$(1EynDHxOV|RUc8`ZA!0obqv3y}P zZCu-GtgSt1$PX!8eg=|f=;`z!?eQ^fqfjvxM(#JnI=Tw%c72!yYBe=+@mevoE^{uh z<%0VXOrT-$5$co^(J)nbOuUe$4nIi}(Rhpn6S`gV6Z!ZMkEqQu%}4j)GSr{@?m;4W zyjtJsZdG8aREBIZgEo;0$YXxp?m^;3ty_hp)ZW?g3p}mMe)C?<3|T_6z+RWCKw{C0~ z1xgy1oo2T?jTjXuXwa(75N!HAXAV$22T%>z5pHu z{FAx-nP(BU*XYPI5jtQch`urovQcktBpE+i-`xc_>*j*WF;zX=G}xL-i4bOKWyfqx zcgG&MN_WLi52v%OBQk)GPko;fIxdWsKis31Goq;CmBgT(Keci^D(qz7W67sKCAN

22SBEWo160W{AnwtPa@0HjBh5hz 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%0Dw%T;GU^o`a=!-H> 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 XvOJH literal 0 HcmV?d00001 diff --git a/libs/A3DModelsBase-2.5.2.swc b/libs/A3DModelsBase-2.5.2.swc new file mode 100644 index 0000000000000000000000000000000000000000..a57e645d0059f906b4e237a552654363cb65e9ee GIT binary patch literal 199322 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²}Y6YOc6CB`N+g?Sw`}ahGgHaoW3Gp-;h+Z1x}j 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 m{MoS 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 aUV+y`b<}$ZhWxz=xo*}#z;!nG*ZUu*Kpyx2 literal 0 HcmV?d00001 diff --git a/libs/OSGIBase.swc b/libs/OSGIBase.swc new file mode 100644 index 0000000000000000000000000000000000000000..797e09b9b7a87faaf456f814e2709e3693cc092c GIT binary patch literal 35575 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

U4kSllC+#(&`8E4ocLVnV^RxGo#TPz4UqjeL*ST z(0tnIJ|bP&a-_6?vy7Y_^2Myj{#M4h2M6d~KM&PYL{_?l%ZjFtBVcFfiO# zQ}WLl`85+&0DU{a@P+mZHUI}Mn#EgXI>kRcYZ~J_MvX7&Z!PE z$~EphzCmBVkBTvh!JTjHZOhoB%~AAXLCK3b*W&!!f65jbefKlL1UKZjdSgr2sTF zJSU###mLFyh4XP%%gU48(Yb0W{z#gej}Ql$yw&Fq_oXy^A-R&dL5o$A%6MNd+Tn~w zwF+ElcqcyK&2SsQ%;j}z?@&<<4nU;0Jf0MLSDVzz|70(4K;aMw!{l{%KX&5HB>c!?Fb|Ob~cy18VN+73OkN?fpPvPOK`@yHtt%^azT6Yw|P{%qqxP7M4 zr%co57Pm(3!CP5hpUyi7p=^-_NTXXc-%TbIvRyVjA8x@4W+j1^JL?z!iEhXV&8stX zFmCYvkcb*&8fBEb;2A2j7jmg5uw~p(+CjMte3rc_z08+~q^5&oG!4INA!QbzM?)@G zKDwAM-TU`*aG4Z&P~RAZW0^xS>owmmxS}>6as9AU=ab%n6?M!?e7Lsfq_<_1v|iw= zdcM`@beaY=wZR&Z50 zRNaE)hf({iwxS}mGxizoK+V=GH?~n?Zej&2>DC|BSsy>W|6mJ(C8#7PlAE z+0XL*O52d?>Zk9)#vx*We~=IIcxc5Vq|SF)3w0}OTr2Ylsh;BAZ#^(f)4cYphW%%A z_dE^0nh`a1S;Ei-W;h-Q^Ul3(sW;%CO(CwqP^bkGU7# zfoJy=C&aY+W&jCnZ-U1U=Q6w7m$_~rj{%?^ry=w*)C)RsH7!{?AhzLnpY^%-?2rU_ zxD+JhYKyo!%YH=8BtqbYSdMcl2lJ$L%A;=j$l~V(S0vRC7wlKOKPqDKR_S}1kVW92 z-)gO51y{elTqh~hXf20qaRU?-x}i225ch;xE-}Nh*Mz`t`0IoBxAEk@<6Z?c%`;#B zz7g*!)Of&s*?6D`xV5$fi%8B|)q^kyg;}04?#=KDmk|)sLyommfk)O)lD$hu-Me6N z%x^{K8#1PyUe*!?IvP?plhE|KJ){}m#>d|5oPhpJhL$B7z&>q4<+2c`4J*-n(uLBYhe;_B`>C(bKKv zDyb&Z9Wk}XPzWXq_nc2hl4CABUoGR$_o}4i%&{qUlxbAs$M1$h#2mf8rkEJj>oS2n!==2FWLA1_+Nh9PqVfT`8IEgRxcR`a z{)%`Y(Z|^|-$PTCGL}2|A;`Eyk3t!bt!QgrexJKoZ1j6HkQr(pU>4Bv+??9D0&rJm zaNr!c6DfF)hWOpx)l-j)PA*+St|= zjniTy{*`AJ)0e)J1p4_Xv>{?Hq9d0?!TJ3#b2O5h3^^WR-3TcH)x=H_KcTAPn?&u) zW^$da*i-5+{ES1a8ql#b{!QPi=1&b8)~Yn6@b(We`anSm2Y9<32*REAo18kZH?Hkp z8FR-;HObPMOk1)Z=6%{{p7&FXjlQUS>vTQBX%Q>&wML-VP@lH%@9{l6U(~Y6kOlHp z`l@p5%tkebR)n1qmnYVVG%_Yzc9q$b;d3{()$?PNOw_ASF8Oq?RQ92!vOT#*en0RG zv*hv$jbJ^7ak$}ZG2y@QNVF@@EC&>J@~WQN)l!erYbaEc4SuzrkXWVI+3vI5HWfUG z3N28T>erhJz|+=JGa_xXZc)*#?~8p4c++8vGxLQG30mxjdTXyl62n!x!~B?HPE;F- zo0r}o{b#>WpX_bC!och%Q=MJ6zz`0ydD2L@8>PG!daYGihF;WTh)t@#!Am;z?xE?d z&s%YjUWCH+nUk}37s{B~+uj5ZeyZiMD%ukwf?^iMc_frKxoexywho!sKs^R+D!y#w z=Un?XFYfuS8$uz>itj_0JjjV4vQ^CSc1XyUe4*43*xZ!U-gNx< ziUs@%c?LT(N&ZQnmTLibT#*$k^8poqO@$ovoSuxDQwkQYBSx;^=( z3vl7mOoLbUtsg@)O54@P%7u1;r3q5Nf!!(Tha{^X_Vn<$Xl}vw64#89C&8CKL!neO zof6$h2ZGfff!oz^WIVZiUdm&REj_iUh1085Y{m7(A+ygkqXKo! zR)VFL!<4cgS;bhJ0PsTy5UKv@mfk0Ybt2V_SjO^mDay2Rv-18r4;_)qGE4W0`Ffej z89S`_Jetfeqr}mTgVk0Qw2YKl6zRfRBl=ct%-wJl8&wD_b~KYSm$Q}2jJE)a$E8WN zc;=&iqlIKhA)ikM@}JGaezMAHB8Rlq9t9lj&hzIdoQJ-M1cS(L8*El3&MnhDEwYlK ztu#TVTEVRz>LdeIGb2ZC?h^A{5%Lm(@noeWcVU4JW@D+tc3`<@QNqR78*jqJfYKZZ z<8@iitRe~G7qbDZi&4rF{E-Mq?ypq@p>jF zkp}Epw2zZakogIXx3NL~Y4zQoDFJv2t{EuyE_FE8Idaw$LNKTRw0mu&xm~F^6vADP z92aueRN7SZxsXUAS{}%zlthIHJFD8AaN*?Pt*r_?^N!WV@(<%&(_POek81@wx#1$8+73>^928hH5;J%IIWazP?pA@FvD^9iDHb zgnELMIZVd@8(R%)#~NR1OmBTc)69<28UJHp2&p3#s}?-+rAoMoM;N}S>q_9wEZXcd zy3fYP@qUSM#ArHM0}KMuk)e~m8ls%&$Dn{aBLqW?bG#tSO3zCce7;6VPkdRfs61sn zs(ElTGZLi_TDxFodoy7!W@zrB9q*C@G#x+1x`~E-a5bq)-O_G)>;09iI`8Q^TUu@J z?#n=~!@EIJcOJAMM>HF}+YLiD){Z3OI)kl!xuD&qtfYikEh)gBnfS`(sHd6eBK9FM zcpChBe97gU&AyW;d|#gZ@r>R_SxY)`GrF(Tv&WB=@Z1o5bb*ga=}B*-K(X)7!te8n z7O#tPlcuBOqF(#Z210>U&C6HTsUCK*CXQ%5S)$meu}vUYC{UBA+L&s?)@b@?^6 z)rRvLx;5Ww*TgSWdWh4Re(npXj*oAl7Qd3kP1enDNMpGCtW}$2D*=9iZaK#oLrZrE z0gph5UhL*#D04Fmwrt}rL$4L5HZ(zss6~NI$`>jefqJ>(=p75o$T`N)Lzp}hAzWBB z$-Wb@(00jy0A)sSZThC4)3YbTQ?PKYoSEqQpeKOeAjl~$qZFm&%Z)_rV-oF-KJ!CV zJKwRO1sd6D>2xa56QMVnTeX_peMFC5J0y=|dza8k4a$BsbWR~*Pc_T#&TbeEhv@fDN zM#yrDtG=^*qp*$obV}H}td<-VMI6E-QU|w`h8wP|$4Zk}=v&07pS@DIZt39fWZo?} zz?iud`p~azg7T7l!vdOZj`}1$Fgyn7AtZOnv~UV!#Svhc1c>w=Q3VZTiFMSg)N~Ck zx2tFPE!A8lxi*>-)A`m(62Ym_vM%*iTznVvgaE0x)l;~ox-;pJh3o=U2NxBZH+C7x zs{?hkkq5EMWTk6zk#ze6!M%Nhb|Hg?cEwopLi3Pz&5%3|9lM-Nr|( zlL;?QBeGt8{vI};=JHYY!+?Pu0{$YiqNpkf{3CAGS_Q3eVs*LtbwR$*_5EfsHZR)> zpIoeMK^oPLy&|uK6fVc7hdml=1}kiwE4boo+z)YVodS6aAu*r7v|L<6+Bt*rlOjCd zrZoa=oOAT*bNt-g@i7OzPYSFtqLcL#tM|+XZoLALlT}!I#(p+obX6?cKmTaSBrS zJYEwhFK;iT!xl9>a-EPLZjSqysP2gjhl0!Ip939u(C2Q5)7#K53pt`kYv;N1vj^k* z5Cgb{J)c+4n!CR(udgpT5mTbWOISfRm31#$u+oNt6#?89^B$b#50R+Aukaxjt_Q3u zwxynRt26==q_K7?OD%QTK3jVeUBIbw;FT|aJIZwUw37KpRH%=ssOXbNAdd9mt*r7&HOr6N0Mj)?PMSW7mg5i!q zKnUUAb9;!xD<4BuJkMRFfsUl)nY_HNId;E&lT~uI>6G0i&$RJQ(a@4f=>h!Z^IW&_ ze&=KgV*b=HXopzSUy5dTi7nS%65nj9-cUwvmZ!v!t2I=6)Pd-`N;R9YbmL}s)N_omsqx}o#V@?gDvpJfo6Z# z@}wzTGF9yKo)W3meI&_>fRBSP0z3XJ@*~Hm?mji<{MH)kbA}y$wAHVEK65?7%CgJ_ zWHc}{$dd0TD3wGdw?baFh$8>nx9D7PP?1hMB*B2z* zwKGdLF$v7!J|qgkix%%bDFap|hN=o{I}#)fhswT=QQ3Y@O7`foDTZ+gwz`njr~A$n zA6#x6$r1U92S#j>D07Tpd`3vB%t1cDS)cxXBx;6iNH|Il5~rvJjBd@svSI^u#mQ*0 z{TRud2@wTMBt^(;iO~P8AABJ7$GW+BBb4^;CQK$)Z0PY zQ~Yp64c@P!6dmwr@6{{Ei@otjNCR(a0$J3*R)~aC3SHE3UpO!;M_?cezajj|6d8+_ z_Xes~4cw#{>lEw}&m$S;-X5E>%dqUI!zfwfHb*H&$+vOA7iJSeCm^MAsA2E?21y4c zm36*l4Km%5Zq-OWNYYKrWP_qjN_OwGcE2+1J{+eB0-}C}f4Ws&H%eS~Qlwoxf|i7= zp-Y^@Ub8}GPm;t(N~!HzM`1*-eDyZ1Bl7ZG$UM7iC%N*Xsbn04bs~G2dRnWA%~jKL z0+=N1{W=reE$n0d3h+?#9?!Z6MB|O~HFku}>id_;SnE*O5&7v!zLO@I@EV}SOnL(l zUt{u!p4rqQn-ytFpbIq_W_NxRp&v3tI0TdlV;b=Y7+oEZILZqW+G+GRt!uy*KRrMY z(qNQkW1O9>l)_jYU^S~!F1l6IIfNpkx1QKtQapeBq%tsjkFX*s|6{@CgBJA98w&}W z#T%?(4@N5uu{FAigFZIoie8QgVtcCxl%l;*{fXMg>|@CRu2n^&({CEkm2{s>l4J>F>MK1}IBA;w z@Q0#8XgQ96#;aN^dr?(=p+%3?TTRi`K?)8^ z>ktgEnJ}GGTM1~gF7u~yERrz}SpGYBs5Xu7!rtTtf#z~*x0#MUIzxv0l-P_cL$Kil zT3B?&2lZj^rN8s#tSBHciP-*9Os(I>@=)tKBg5cHi%9R6`2D20w~VBDZHz_Hrc;e7 zTkZjYOwI^JzXbH@Z4BJ7HRC3n#t|%!KBqxsIs=X)1t(6F_*ZI{x@jD@29kxE3s1Wn zelis;EnPxqEUUwL`kB1)ar4yJI-P#`T*WFTj}fM(XFz$r{&rucaThlUI@B!NPw?Mg ze4OrWrk=lg-M=onzd2Hn7WuDFVbedpakl#-hip5+f)c=t6tsy?ak>%5NC;N!TE^?L z4=)Ed@+PWaqc(S~(^a#rqxa#7%9)Olm^pL`J;?RgO}7*vo9Q1?P)6rL_1W?5)IwHp zu}0h&-UzrjHXAQ$%*xsGp&X*+QEA04#?1gaXD@w~xyQKPLl%%gA$0EJRo&Nnh~LMc zw%NO!@~e0K>!SbnV-PcUGIa#n|6>s5(JTlMENJ3KJ6JSLqu_ezSO7lZ+MK8cV`e`1 z(E*9uQwe3`CD2Cqk3A~;59d~-a27Cmu!~y!OI?=?x$f5Drs3KaV%P|S*-mn)LAB00 z6>5({pV7E%z(RJN@6-tETI8fnK;}ASdG(m#HvHkncn(c#dK8$CAoKvw?)TQfX>9%W zPzD~{9uv6u3qD6xtn?Qy!%*OGxv#?t?+6rQ9;wSRGnp{k|5R5O0{728-eC` zaf!2p!iI&Dw?u~+A1$Xox=n5?T~S^{YHlWGoRfL&1T*!A_DLo4@8ZAkUu5EzS{I@K zoEl$tIWE{U^Q?n_h_~T)R2s;NZbTAz`!n5ed#TPc9f_z9Zz=Y?X0pemtTHj}`3=Qe zC1Qs}nj(jL29^+(Ik^0{bO)7wB9o@9$nrjKYqrXd@}@eLs*1u+sF#O#ttB*MpTU!JG();}bdS-0Agdl4Z?lVVs{QmOA&)lK_=9k#@e8{?tU zieqZzGhqUs4lMM=uBF>$Hb>nKqWq%eQP#qO(#clq(PrssMk1&-Miy#VnkH-V^ZjOo z{ScO>#AL4?7wNhwn<*y{m=AVpvr5Sq4f)$p{W$y+Np9BIN4cfrhm| zL-j>zq*ZAYjwj7rUEiYLoLS|d^D4>Nm;0056y}QzqPyGcmnc`XqLiUO=V5lvzrV_7 z%eV9Zu7#!T>si{T_Gci?6NY=_r6~CpxPO5+s*T?sjofTyuSqrwGPG2ZIeE80hx*8_ zM-^TJ>!`%IcLMtFJcIba(<>5mq~fy(Ns3S<+SCmFnJLSONlT z1*);35oLP2P=ym4cs&hPFZY$0Q9=ewOU6;-K&vziS#1#YM27pCnd-h4angp!ub2*4 z+?}77XcQkyi$ki+SCr-tm6)nCKQup_k-E}D+rU>kR^G*x{m>q8IvdDaqi>^8f05VH}7xG?ez^ZQbI_8nRjK!AaTLW64)M_g4S4q830) z7svm8ItQugEB@N=-ls3&?rJJ2KTJa5yJBOd*dGKEZHe?MqV)#E4t4MHNfV=+q{LH=gjcwBGvGZ6$WJtUaQ@d0je>dohI1?s`fAI3C zioRP(r^ujtOW7R#Y*6@R(uP14=J=CsOVuTPvgfAL53jl#Mr+*nKozMZvGlo=lw|*4 zEsX1|;@#Nb-5$cZSczgLD>*i(n6i9XCo_~%vA)P0FLq%!tM_BYxd7S&6lw3w#L)Hp z>v#+QQ>7Wn%jxfW`9IOuhScB*Hq5L95ZZc1k!fLezj$lhZG*D~i*3h}F~UYwe!{M3 zR)VM4A91CaUogR@oG2}5d&dx=CR(1mX{Bu-2-a?gg3n2Sf|tc}4aY2XJ~W(^OEaq& z%MPBYYh1yI7tDhlI{+adg!>W`IE~_vkZrCqPLwz2oOe!woHhv8sSHyG!+AWwUc!#y z97K;}EQIL<>N3__&zyqBpeIl=Mit_0#Ly&^2)pmQ)N+22;g@dRPF2S}8Hf9UU7l{xn7Y9e0ME1jwm3>zi z=%ahR9C!UP@|+w`NwPBp@s&xBP90+xm`}`~&WwzijA6o_!O}X7YmLq^F;aUVB;4eI zX`3!FKY}`gu}(>ELUXmN#4f^^_$mXE*!kkA!Yi)&b8+JrWg12|G71Pby`%3B80(Sp zpr?H4aL&7|I+2QDaE3xN$pu6^@pmciqTBFg6in zeF{^T#pI^S*Cgrddw$38wqx$rRWTaM$Om1d<3!4z4>)^Ox~HGe-tI%8?uX$-Ze~jy z`#l69F5_={nh4I_?^Y3RN%)72_kfC#-i`$Hq;=-CE;eqqYuhnM3Zy)*Mr}ndi`gm| zg%`qSMF8$-nnt?TDP}LDtVVmLEgK?#Y~`AsBhBx5+u2$JR@mm;ww?$++PRske_Z!g z!ghMwBw4iR_o%R3VWf@mX>5=)7p*p!_6?=8E@u9x0CR{kF}QcsZgmyfZzmYePH@+p zBxO>ypet_=-3gt&H(&ThmV~3;IWgUpa-<%&^`y(<67S4%-6q2j8R<6lpW5$l?DT-k z(A~9{zPxgbm)QdA?SGjg6ZHSAE%`?+ikzLXnYp9-ANRkEu}Mu|7VztNyY>_wp%@k{ zpT{IlqXwh_kIP?NU|p?r0DcQxk`@lnRgN_gq`vM{dVriyZ6rvT{NtJHae7S9#E>j- zZSHDu`tC>OBg=|%FWpxp=_a{SQ}g_Vv9t8jr0I-!+!N>Jnbqu;mMh+=A%5$Cu7>UoysR_zP48;gn@Xlt-4S z#V%MN$(=xDN>FOpyUjYYXfOATP!(=&HoXv&BZ^mDSv)dgexBJuI3Tq61T2G}C2zAoqULAerM)J8PxXH~cthWH!sshkXQ)FQ^ zk2r5RW|LBrV3`c>HvXQJb~z+Wz3+b@@o z+oNbq6$;P7FHTWGu=py~@9v*a<4 z*QK|x5s(^Hm+$WH?>n2n-c%U8u^N|BXwW{AHZtXN14M;$@@uP#+PW}&>)A&BS1 z<9z064{j9h8zygbMsS}bOYNN)-LPgWe&MB0 zw_OBvs`C?>37pPT<{pX7od>PACC;~fnm(xX^5n-R0?#){i4WDgbTNu7vze-~pg7GD z#7v2J7P!d`MvPvXQkqgXueNNV_Fm#ck*b-+1A#d9NL(KY4@X|K5%IA2#;t z#r7x7>FvLL%_+%3!~A2{Vt|?c`aA5@fAszjP)i30wrBSZ`X&GXtt|imP)h>@6aWYS z2ml9=+dcpQ0000000000000XB003-hVsc?}c`kEzW&tTP*33g!QwphY004Lay?qCG z8^@LZ&K4{HHn4#dB~p+?b%+GOPFsa4kVzHFq~sci0ZFVtiv(x@R7s^B%ZZcNafGHW@e|ndDGs! zdGF2egdqP+5Q-lZglZ=2EL|c9!t2Y|GeNjKHLP{^?F{=)jEyGJo%q_kHlE3hcQ!U2 zKYqO7cvC|%b)+%a(b3Tu2sMU6n~-Bu`eY&#J+Uc~UbS`$2e31i9!iajXGW3<9|4RG zCMPnR*RD084iDvkj!&dUImqFm#@J|VESAWm8-oo&02&_Z)RL*OXl6@ve0+3dC`#ZO zPi#uZlS4<3M~}reX`|6}{PIRSBbCXFWJY6Kdi!^eY>%d6Jk^Mz#NlWrwq+l%>X}IR zLT$caduJff8Eo=hyK7JXL0>2k4Du2@-{p;jdtO_%7%}LcEy8lo#FF}8{oX)*Uub`F zBzdB5SMSNTtM|8dHTJx!y*nsmOJ2Y6W8b{|h3B6v|8D%t*JYPKzti(;>2hI*c|?U!{7Krf6fLl1W#4$K>`9obTku7CD86;(Z-3)$Y{FpKq@&NOJzo4 zX~82jP^qS6dAS;RuFkw%H#WwTW3k4g6Qd(XlgAo|lEWj3BaP#!A8lq_#eHV4~ z4pIec^YYHaEw3@s(b!4PEGYHCKxqCTPy?N~Myi zWxK}4GbepKCGCr*V!lK&cKQmhw>#U%BC8h#l}Q?XRW&eh zBsq{t4xrCQ5+k%42ja2OaakJ~ja~eC)|iLXyxEU=CXFZ4Vq35vZ&0ORM{9B3H^xZ{ zwKWyw^{&*^5h$9~IZ{h&z-hHnYHMzC+kLalYBG($+;&UBK$FBUTb+W&I11u4V)0m@KFWbPKvOJ3YxZ94kd5)<;+m z6DC<))LkebiYQQl0tHk+6$D8(GWGu@Y9USIpd9<`x9KGNSlPdlV+=UMf5jmGml9u84}P^rO_0Y`}Nw z%sEye)SP3LY^h!oQ8hIpYjQu^E=;Z=hYZk10NQ7Qt`SzCeQc#))K{E(USDa$TW7;tHGQni34H6P z{Y$5iYxVSuf7ul3ozbKzidab%ec!0aU#qX3p7PgCp*j^~B`wVcP@xnljWCS`$hk(S zje8Ks(AcLco>k{>DBXLb6A2LZ)ZUSFCYFe$ayyEUx}=-NW%FvrYCe#++B@fE$fpJK zQ#+H19m#|?a%68ZO50mMcC8F2qx}~V>W<{t_-HH>Q$bME(Ic^oi80?k%q^*4@MI>I zrl!P(-8_6SnTd{G%%*_A9l>^xmM2CMng1i;y(5FEXbKETe&=I5&2cXC$LmE$b1qD) z>-#Pe*lyarqN5|PiVa_RF+iR)+s=paBDdWdtKR<`qW!Vdu~_O*B6=)3G8!EmjqSe} z@OlArl+V<~$Fy`ig~C^rHO7#V1+$Et-#ijsa<^nuDt~S*2jGd2T!S1d)O* z?Q;uRqb!Aj1aR&G;1o>?bp&eWr{(HvYHIf8CFf0Ysi{3!X(i2?tWtBRc>$O?beh`( z<;ldn$wNF-nH6tO0;x3+D99Vw(s{!!HV0)db5?o-f(Lw%%A6_#Bji=ZWvW=Giau3b zsfzWgxJDJ%sp1Az^s8cnDg}%A2F9@Mg9TwsR3{xGlca)4ms?;(h@}iAl;SZ`N_k2d zV#~o5Rp7^j!YZ-4X30|S)^c|_cUN$C6?a#2cP)3tJYY<9z!T&!0b!z5aj1io9Xyw6YW&=<4g* zH?VWp_Cvds;n?8Bk!mBRZ&!csp}rlv`Um=Ydk+pA>f7tW{*atV4aL$PBg>B7{o(H2 zl;jxVn{7q^uD+{w^$qkN>^gX;e_(%a-@dNBjs%_9N2_P2^&;Qi-maaycDj=}im_yN zoYtqLd8?ty(>`@6%t`VS85*xS|LKd@)lweEe&v0%8%g^T6QiTc>@*u;D^aFXzb9|rG$hkUMr3l_ z;}*=ci_e>V$>Er5Z}Ld*L}q*<;{q;5$Qd0T-Vq-e9WLBS)5|x8e4RcC>GNqp(awpn zv6B|}Icr0(x~$O(6md~c!|+IYd^CE}yVLLuAp>|na}a17j`2aL7>gc_^;uK6FPY3> z;(Bs(+Ki~p)#P7qp7ZpisLL{ z8(gr`@f1WunONsSt)lf$)rF{js5m)JW!}U97l& z*N#Jd-3PBV3KvwjR{&X21?TK-p z5s5=ir)sn}?J)d;yzCwj$#g8Q{|fEXTc@uGIk(fvj-Tc(#N!pv^e{pEKa8%`=VfzI zsb4^vnkVPtf<|#GVlRf5u9?Y!%DSlUgW&dvgkaR+YMH`9EO&nJQW)QdpTiJxI$(EWKgTSvRY5Wm-9Nc>>-|ZHod~a4+WN+%~@w zdJzz2t7#RX^LaVi1^40r0xgp*aI=_ZsQC=`5bXDbgeMpOu#``MbNst0E|EG~TIFCM z&;h3nPOu41C=i0v)+7g;n*(yNwJ8Xv1D|aW76jYc+T~z-D4=dnCP!n@1U{Mpqn(O( z1e@WsAZtf^hYUF6Ab%(r48Un?l|vz935A;5<)%<`o7~hK3dv0!p@7`n(%vpNw>3A* zEgiw2+}hICEVs6{x5#a+0N>V*V(pNP$n8{HM|)F1?&xR@D8RkF-D!1jnH50uaJ1{h zCZdcGKhM%^#0~y91wI0wt~m76Jj6Oxo}pKBq#=99E*VdzGi)ncrSR-dEfqaN3PtaH z(2W2uK@VTEKoV^+I3+j@pq)PEb`aoBpMy9Z^04uA`cz&?r_XIw)9G8f05nS*hUPZ4 zG?UvNgxefwCAW!(TX?uVq+k%+5EE<(wp7o7l{+AoLS;y5)`M6*VXo76=LHR_NGr>6 zFbZ{b)HQ~@Bczme9#5u*uTDkBi@SD0d@yjZm*fq*cNHfS;P_(02cesUEWWI7*FFdt zb`FI5diNPQz)kk?{a`qZV+_x=@V;rXpmj*kpy;%<(_)f9BZVK9F|GG!B{Ne<7||nW}3Ue z=i-+uo@1|IF|ugSScCz4I~Hk62xL0c+5$q1owNyVdx*jvfp&5`K&+br9azpyu$Cd0 zhnw24*qfSL$ZctDgWKBLRKgi(lCVWHq#iDsC2T7N#~K482%DssA%0ubMnVp?v~)-v zgiD|$h#CS-O~56H4VGMvX-g~N+uYi+Okqlux71r*Rqb5DEh%ceOT8<-4cc>RoyJGcsMLEyd!nVL*@(-YY7rDN{!Bq}! z(mJqWm6f=>OqG_YtX7rk)PfZ%Tct|%OOY3b7;99vR+ZMNY=g?mRaT+OFv{?&tWqs( zR0{(t3#u%nvSw9kRmBcf>Qtr6RmT?9u~n6}sg5pH+OA5wRp|<~s7Ec@tFnD+@qSf0 zpt37fsZTBLSEYk0JERt0rAk+;#n-6PwJN(#ExBHm22|;lYDrX;hEz{XEg4m%F;_xV zrDVyt>qb#6NvS32l1%Xg5Iv?!CscN_|8Ax#M^z=RDt93C zHvI0y?>+dv7eA!3djagjC~zOb_rrexzmMSeQGDM6ALR-kfN z>1X);CnKAj2N>H!&Q@~Vsssq~9TWVey>)g_rZHVexDT8>QBM{41S-* z?{lJB^?CSD!2bgLFX8uP{GP)1)9|0e?<@Fy6~C{c>~}=9X02MY!L?CRB}@a1dsLM$ zauPa1BJ8Mx(yEkFCEzTfTZ;cfR15zTzyA_92tuhK2}_q%Zz5Bn@`_5q*CGh+9fIJC z2+Y?uIC+@)8s5w%HMVuj``FD#1@{M`PV#*g`X^u86O1Q6Maj1@_t)9v(@gOFn2F4H zFZ(IQ`a=K0J@?Pqn zg^~o{&-uqMs4{}P`xi{Si~TG6CHoc21%GV^e`5##%?|$74*t##{@xD$yB$1d2X#C6 zf*t%1JNRFA@DFzIk9P1+cJR-3@W1WgU+myt?cm?+;NR`w|JcFvc5qV6^}|he@Mb&s z20M6*9Xw?RZ?%JOw1aQ5gSXkiH`~Ft*ul5j!Q1WNNA2Lp?BK`k;6rxs6L#=nJNSql zeAEtp!w!Da4t~oHe%lWI(hmNs9sHFY{IwnYjUD_qJNR2W_&YoJdpr2=cJQ1X)a~F4 z;vYnF(*Dto{gWO0XFK-4?byHAv46E=|7OSjU6eo09^%%0b>3~lQh_xXWZ349F#Xa0B*}F}65Q&~Ew+SDCUplr;*dwsA zf^EV_@maoUo3K$}6<2K&ZWLJMO^o~Ak1^7USJjeMym~ol#g}xDR=g%aTJfa^Nh`iA zLt63LYe*|z_iECLFF#3Iao>2CP{>xi5g>}#%H}Sin5}wMmr%m$E4u^_TfG7;@(N|n zhfx>sT=R(GuYJ_;*RA5w>mM^dH+<6YH?Hdv%9;OAmr%hrl|9Q~G3k}>LmgGD;dAh+ zS>qG%maxE+@M>6a394DjzRH#rJqHNrm(bUcP|KRW4zG?ie*@lf*78kwKGymzcq>@j zx8bd1?cag7igkP!UOnsl9=z4;vhTxN!!FOlTgx_o1>QQg1w9aEZV7e-Vpw!54Ay*M z3H%8Y<*@8_;Zq4K?gGYSN7&&mVoXuPD&=!H!%nw{VG|ZAX=K-9s9{JTZr!$Gr^v*v z?PWV9QQWa}$xc}kcZD}#I3)RcGLjU9+QZB*#To20S>0jLuf)Z3F#lYBSn{iJ=^S%P zD-O$kS6n{F-15r9ioYPPoMVN`s>2R{QQUEk6+6}(R#8lav8B51u+#5}J7Kth3S{L8 z_ClGyo$4tUw!^TJ!KoCt>#f;{OLIlsnwu;u;|0hA`_LWJz6!F_+!-nKFYhUYoo0B( zh}n%8ACKwIMv8ihgm|&ulbz8@$h=YW#7p%(S*^4TmL2-uY^1EGOo*53`=+y6dAI_G znY~&6dZbT$3i)tX5~hx;`6TF09vIf!t`WtlUrSJxsQqXV0-VVc;BVXRp); zA}g|5mA0~H1)E&)y#7l1D2a%gsCi&L8buqiTc2Z>iG%v!w6>~VfaBjBu4naZD!f`e zbygobO_r!rr_OlV(Zt~yZB4!K=&=pQ$_mN!wO$CXW&X_%-F>VKmcM#z0T?qVs~?#T zuM=j-P*j#hS-FRhy@3X2r*PE2OFwGhC25l8nevCHBI~vFm?~s&I-2!&Pa91h%SJXv z{L?*t*0YhtH|dG2wkgt}HE4}lEf5Zh>-8kXgtU-`ero11;}p}PwP>wY%#9S&rnPD9 zS*?TTNoBQ~a3?x5jm}iafVdXuO%QsAg$ex_lBc!HrnSq%n^DJcWOB%ESve&m*GcsF zk#ImCY(ISjxyJ`3|^8mAjb!DynZA>U&LO3y{&e z!duwnmNWWmvp%%ybu(dD{O&lbPqLZQlRLDX;a$QhSpBYom9G8gjnMI4&LjA9;GE3GY~RM7)oX0_;a zE`93!dGX%!=N~(N9wGhBEXxtNLhFwA=x+T zC_rfMBWM53LoUx!GB0)wI#*FY>2Cwddn0=WmGB-G?!}_GgN0nq(jUR3*yVKUZ$~w+ zMGpV#!Ur$~Y1O>btZS>bmDhEp{w`#?iSfj{>}va{#(t_XoL3{r#4XHmH`DKC*+ED6 zAfqwT?*SH94yqi1cOyX!9}+mc_W(5Y>QzSFcX8yi|f<&-%Ll)R?T_Z+*Su)R>|_fUn54J=Y2ZUw;sg z&k`W~MD$L?^beC6IZdb-X3J>|%$TZwgfao8QB}K+Tui(00OtFHj7+{yo3`a2BlvqT z)M!Z#>?;u_@X-@D4=IsBZO~u3ot^cpqhU}6Zx_APDe;)3iJMg+wz+#oov6^;t(7GYFy?!k5k3R{OEdjvzK zKZ=1ZCBt|B``Iav4F3a+tomIJMOOYs#OhPXMl(tHG{)#dj6Du-iiw|tS0O#Y^iR`- z%4%bpXL9TF`s4VjizKu}c3Mk@$64e?|ATB=OHGAuWZ|?(6!9}`CL7Lx#GTPUi#hi( zBu`BFKhCCRjG6a27CF{)Oqe`&R{uPkJ{>)-Kfz9GCnlE-$@&*CY8K%;$w}B3DaIsx zui`OZq8O9#88J^%j7j)j&GUSjosRftKu;sDSrd7!_FC__1RZ>eogo)brSm; z)4#?d%ciwAu<$ZAx$KPobv9$S;uZq%4aDSH!DGIOm|QD(%(rMZ+?4BX?G&4e-^BE9 zv+Rbm+St<@hGa~v=osCEz>sKbDlpIW?-0J>TN#$@cd>NeNP+M1xHlo_!N}`b#M6?V zp+*#{*i!GKM*M&`;%O%Q0Ig_e&%=F&iD#JpJd51MdTzs2lH1rB{S10yP5P{UmYuG5 z7Oe!lA2Qk<-^@V4{sE2HLo;BCh#p1p&GbiBbiIH=_lDoX#9Rjb$5x`51Y&5G)qg^e zqi<#UPuUD6X^}%x-1m#X^=H8K>J86hqJD!3Kf_A(P2SExFn)m``XYP>kli}Q^k3o| z%Z6!hV-ct34ByGvDNvbzrAYDaGTblGe}y^u9fQRFn(fwqhos2u%$OvUEBtoG!nZSK zRPcM|SuF_Jz;xg`VZ)gXfsLr--&v$GCW3~&iy5T#9NS&%6!jMXIcq@v4`5YVhLgJf zp9qy}caa;uo4V`|RN=LpE$F$HftbnqA1R6mcMtXltgoJXm^6701JU~vi@cj~m$nPx zcQfSh;zLG@;VD_IAp9Pttk?g{cC%N!`eybH_D%-rk-)YJ?*pUi7T(VU>2@Z3fC=)o z0wfV&PlRF-lirU${fjw?|HAaYGA!p08L0mawSX(Y*A$`qcbYUMR+;l=ne$Aa6zwuM ziB_2pQ<X`Dta3gEd^6< z-g+vNRayBXO@R+^Fm1vIL<)9;x4Tnh9|T|hc2T?^-aAC;0eJ5e{e^$&{E z{=3BRCm2hg(LV%)>xCIGfANR05#g>(^I6Wv1 z%Jo7aq5KgsdpiCI^WZ|5{xO8>g+ZABKf?5n;}iKXt5>u5qfCDo6&bZWf?D2VU;}pH zQGC7E{Br1ziGxc0`Qm=S`y|H`Yy{w~(my2*I*_mi375o&4!>sF* z%;Q0>&sw>t{>PZ78F8OCfY9Q7!hp^ptkScuLFh9~|3@(rUO#i)j_-QNV z85VyU6pjinwO8RXng+`O=MNmtTHy~O1<|2@q{;Fe)Bi*zYH66OknO*bP5+CS^?zRs ze}(Y>D<#)i$$zKh|3UJ!nS7p-mt)cM(l=rJ&anaECY+RC$!?PLnDqOOfzj7C1;p61Mf7` zPQ!baY0p~THDWFO#mi9eg}j1BT~cj)2@>8(1O8OSy&zbSDbKoYLFPyNe!O*E^0k$D zwl~~lzLN~;T|@jTJv9FXdwX{e?6{(9|NdQjxi!6WBmo_LVkjoua7nK!G!m|t5i+n( zS+O9NbTho$#Ad$fKNT$yn%ZFgr zwFu*yeAV1$HW>JOy0`Oobdlzg?B6_d+r@2Q*I%G`r&-+Y2AA2d^`V!1i^I*;rc`;l zf_M5TOk9rm3`BgZgLF+I-};ZYw!!Sy24g-Ui)9susYNebR2mKQ*}yvii;cK)5p~^( zW67gAGmCZW(eZY6UnSpkWm=9WaI6+w53$=-|KEGf)iDB{R?f$`mY95;+$H2LxEgt4 z!@g+7w>rccYOPGUeow`Y(DkHLY`D|sv{JqKqh;r~Ky$4OW@*FPv@~s?lWSobX3$1e zX1;tY%e-|wC)Yk$ew|dfJ@s-6y&Uor2`e|DKkIRk{L!#7S>+d=VPw{7;OxokTHe?E zW6HH!P+h8Z#+8S&t$NM8LZ;{vGE%#PBmnb!Ohh!}JfIoEm*DVgew;(|U2Z;=!f7QsYDR*WE2`%W0JNGg;{B6Lm#s$`nQsIo^@DpZHJ z)&ek6dv7)wa=VV8xSmCR?Vlv#xDr)ykcCfnIy^|eMMQv^Mfys`2 zTZC18V0RCqv;!}q)K*VSZpJH14#9i1c+GX!UeDcEa5uu;0q(w%yHV~Aa(9@!8h4Ly zcZ9n~xjV+)BzJG*ZkoHtxO@Br)pe4GU&X_(=Hb_fQk|buK53scQpiOx+tvV_M)RjcMVgE@(`P8n#1YTHLrD8q<;`b*?hp8(z9i zsD-9>8CUL>3Ucu_wgwz53yakhUkNfMa2_DZN5r9x=`lhTVPiuu*k+0(2q(g&7R&R_HYsI7(^w%LNTq3R41G%Ub)0&4z zg^ZHZ(IUMGae%Bh&-lvs6O@%=i{4T%q|bWVAbvi_R!OaT>&$7bh$`0GbHM5&rI4kR zLVi*LxoPL1oDG-Z1le->>GavtGg^7&Gw08HNV2vCZCVdxru-YGc+gdm%3;}rmMXcH zUOlK}!&PW$-=O?7N<)2t#y$yk)pdl#4Mk9i=8CSz>ohG!^A4kVcn3*VzCq2?Xh3T) znoDRjI|S_<>tvdKB&#{5X+ZBp3*z)KgE1P(YAVH+4;!k|8_u!IrK3<^@@Rn3kD{-X z(>D(}NQ~qfbPOu12(mtk<}Af;8G%XU(Aqjp@&0X7lmztT_4?6}Ln(a@3J|E0^mHy~ z7f#(6UwZLJKZYLNjt1>8dh`Su3$Amt*qW#7O({b3^+A4I==9%F0R@fT07#D(n#6%t!%f^iKI& zV1rXKzNovQ3PeXx2-0M0`u7-!>+c|FPCAKK0or>_XzykE`%Gw9xGp>%rMwQw_nOJ~ z0{4Be>pRD`i1#umzl@%~yQ~rw-^afqE3_4nCE5~F2%>KvFtbFK%$)97B8FFDf;Qgwl;oQ+gR#!&L8eu-03Kx!WY(kpN4VbXm!{tAF-DIv6fX_P9|=omR&yS^YeeM7$EmtV-%gV`NtHnOT*5j#pA>OQeeE0jrH@)N{ORHw({E zr!q*Q5CJmy6-cz>OG!~P6JG}P4EmuMG7u>M5s0c6MFJ5Z!yOWTNQi?_ENDkU^05Sw zRPL#Y2=r!i#S}PQr7{!ajfP<{zs_n2EY>g2XBEwio%tCvmsTucR(V-om0$z;`8)59 z<=ktO3tT1TS4?NU+P6S!^1Hx1nw92v0KbiwcNdm!43}PS9OdYO8NVX8S@HWuhBv$Y zKJ$j`=1RYB&P7&SgWIh7eRTh0^Q?38LP%LxY!+q0Fa9{pV!}?4!#jGCi`$>c$~FNHkY@Fg#c{6Bof|osLWT((ItjnXN%3uIRNO2Amh$2yn}x$S4NO zr9ll*0-6g&h6kQBDWv?6i0&8<$hjiLks_@qKn~HJ!rTLLw@dR$3|~07 zI+eR1Z4sDd`R^l`&wl1-yZCQ4E$}93UWJ&{i_jI;2uc$jK9oq097)86A;8GJNbus4 zz0&-m{c+s2!y_X$FuM7hf46;pXV5`E|8lq`JklVCiyPVP`@{T(`79A79|K%3%Z)+) zc+9X-QH7ZEN} zWe^6us9B~eOH{FjFMSc5XCcdC4PrqIvr+0y)!IgfjOr1vgxpt_X&NmocCEV`?GQ;SCGz6 zWD(=e2C&9aPYqUa-jbA|^MJ`@fE-$quH2IBU0_Lav5Fx@YMd+ZwpOQCc{JIY1pc;2qHy&7%kzhWsCPy6fz?x#R(knnV=Caax^Vw>=tp>7I)l%Vg|KC@Y zjRnTw6)!Rm?acWjyU;?gb9Ryfg?1=&R~oz&b>!4k{F&%kOt?%k zamFMQ15K@3OG|4g)&!kHdrL>_a7VBs7Htn`Lv5{tbRjp^*4EP0gruhS&~WQ;sHuHe zYmK!uw+#o{n_344IG;CQzH2wok6XQ&m_<{3S6w3b53@@Hxbf0}g`U!Q+Rc1f&Ums2 z4=Lhded9f%BbTOPO#56F-HE>by_~&Jhxr3ZX`FG93QZMZ7c&q^UP?q}SH%$mt1}zJ zMG3>Tmj*j)&9}4%su$=Jjwtk29dpD7#$-3y`?lX z)3CUxA=Da@z`)Q6LxHfNqMCWDg<=vkNDew=;6@fi3{sg2-HcqRN?z4brOHrt)u{4P z5FZc?+`JVe^%g-YxP`uzBC*(0Qp(-(Ww1TNR~`Ra4ii8ZG(Mt29E#u|g^$6fs5pM` zr|&_YoGw^T;Vb}^i_7icrvrYz7hUecEPvvC8fTC7{IrlCC-XSsaGb$x0H+6By5gsY zK%_t`2t*3CLVmsvkO4AxaI7#)qErBTs$%~F=L$$mN##5XV7X$Mj2b}jn=;nNeXR#(VE>xFwme2do9p;;`Bx783 zrC$6Vnze;5HlEgM4z>iOV0(*%*OL~K<}LWV*Gm%XL7s@Wv6~!RtmpEU<1Fva6Y25A zCKl8x3|gZKRjkB(6tTEqX|0k7k1wt~-oMm10eI;&P{qU5SUj}AB(_b!SzZviSdKwR zhcDXRq4vf~gNPp5&^$>Tv>}Z+UY8n@xKcXl!*PhEh=TmBjLYB3x%{ny%ik*bip+`6 z9M)>XlB2V_Gj~@1@FHt!fj7=92B1!O&80n5nDgMW1#qB+LhsdLVjHjP+7GDNFC=Qo zbj8pDG@;Z43`-O9jp9QKt)vC2&7Y>$o1~V(8&Ju;>6UlgpS`VIU@usdabMujvCx#y z7ho?u{f)-v6Qjl2(6~VRXBjnF6lZ~aFF_`7@#jwd;8;|4p+;CFq&?WW(1U+csHsT> zr|?zzHDAw4bkv-L%CmUAuJ&Amixl z3Ep`)iY)G(N2d2B1<ol zEWT7k5axLTm^VO{${e-VVYxpI7VPMjX?2X+Om{1vxyyXEj`WmvcPzX(aq~N$Ycl<( z*8uHxH)+Ae<1XX?z^;qkMB@5g%eMJrU*k5NlA5o|tJX8qylSGi{CiBdjO%Arn;82$ zq;zmR2+dxLjDk2Z$NrJSOTL4$Ahcn7rW}%U{jGJfPqli1Oe2!go~mwbyS!pXdiVaTvVDQAU8)>m z=hIm2%eGdD#4^(fl;v2m?s83bxw?&IpUM3n7~3S-`v4WT&&q0{l#hSqh0fn# z`CUbx#Tb^)g;X)????(WFk38r_))=ZQu7>=0(v3%3aBUvXE-$N;(dsK_g;aVGD>G< zFo71}R59jwO5Q#xNpfsLcmk9_w7sP49mvZVE;Q}1-FvF#c<`|3hv`N>9g#+U0?zp+ zCGLiOROnN5sn`h`{#BdU@lZ22xNW2t|5N3eBV_x7M&;3C-QOX6fd+czdCfsz6A)Xb zu9fW#r=P$YG@Z{sJLnwPQudPQQK>7;AB5ku%0BHsmQ=4MG)!r)ci@vro3pKZ=lF9(9G|l2+g&c~t}t!9$Tai#Xv{^s-1?kjqOwv+3zN2Fr^JJ( zQR#+6(ORO$a$u^4@tuOu-D)P!R^tO7-GvmH*$f+FHEpljfgp#Ji6Uakfy69S@H2$4uj$t9&H1AW%3U#HFrOWsUooC(84rwm0nP+PR| z4$+afdO~`FHHOOg6Iv#zBgTQ%yA#Lo=c03vYf%9>e*3n>FW&~KZ|XXQY2RLEg{at5 z6w}tbhza|PdE_g|tZzAn-rBBh3#WkA>~ zy7wNqX=^5%{Xt8&Z}H03U;8k2E|%Q&%ep^9fcNcD2b_?T;&~v$uwkR1*|7MeqM##| zo>YlRSgYXVL%b99+HRav<`6xgH24P>Of#q+bc=?Ko0tpLyZ<<6>@zNBzU+MKC`^#H zn9y&v9?{e{;AZ}1z_H8yOWQ{}sQH7p@|4R!iRxZZH&MH>pXgHsV!o8N)d+vyzdTV1 z8jx`46Ms|>JCj#ibSM|D!DL<7!bGGjwDH-*`DPQY*oI5>V_&a zvVWo>Dd0f7$J)SV3Tnc<$CubmGd&w|{{mt+2}J19kR-`eU5nVyCVekt>zAgftObl>CnSp#)w zxIS5-0+!qlc^l~`Tf*J^sl4IGIn|}j)z*xu%KP;eO%e(z?jEG%6paw7uL|JE$@-l$ z>&28Z;-LsvSsDc~*3UW?FH4O+m(-o|)&E)v=@8a9-K65ts^Mi*AQ@8CTg(!E77L)0N)YQ=v&Q*rK zG85{Uh}N=7s6|MTg`O4UyUlIGB$mxLyXBRQ=`r-iqJG_@VA2HW&~&_y zf9vhVxpSOk+#of#!v+WdoRZm|i~)aW9#z-&i`&j#ooy}ohfJ-4;PyY#ZvV?FbY4<{t#&vmw_)nhgj zsAg-M@CKNn_)y@h`8AmbkJd+OljGjkaNJE?(ehVX8nn;i6o2 z1Z*hjY<1>y5jt`rQ@(ZQS&5|+8dB08CL%9w2KBU_!AYkHmZ$%IP{eSIhwtwzH7hY{ zB*$2u4QPo|w)ydtdyx^?_gKdqX)ZbV;10ys5iwYG17BEHLTv0c?+^X#q7e0>;@FYP zv>%zFdRCGS1T=!|r9x5lb9d%ROI-biWG0^?UrpTLW~h0~zn)vTH|e04RXuaUe)lV& z{VBiqy}SXnTwyRB{VptSs&M>Hr_>eWymGhG!J|`4RTh+YO8Ga|r`+FmYVFA7j-Twz zdhh14r&vzYoT2o5TrZA0sxA*{}(-CYy+<_L_ks!2M-5@ZaRPe*(A1 z_wT^!Z~$Oifw3LP3IFY$=Xm3n8Aktq55u23Z}%~WxnDMM@4tDpFah##?&4vu7F?~ zcmUlSkdHG>#~uyFp! z@uf8xN_BbMoDlbY;f%%Z9j5NygM!*mZ|EZD+icddywq##fdoC$YOKczgLC1uk z8P@or^tN626C`{x9aOYBaP@R@wkGGh0HV01ptU!NdM`kbPibks`zqk zsvcteQQrrOsVkkHwW(Aef!vt{mDmahrL;I+NM`dPR=ab~-R}zDcaQ2<;2^ka1lQ3n zdyClQsPZyQQ$<}aDu(F!uhyA^;-co{HUkyV$_@J)CwF90SYdjXS^$KioNo{`q=%j( z@~eY}<#VOY{WZkl=Z-U)a*$B>)B;~c33=P$%QS894=ItHpcW>!>XV45I~KgRmG*FzBe%fWF*R+-#Yfn|UDwqsmZsjP{UmFgiQa9=q;9per)t!pDB9?rA2aJiQ z@6GA znBjZ?{5<4=y~yygncnIje*Jbx?V^mu!0UqLK&_GojGX4EsY+mHH;YFBM-JDS55?)@ z1^$Mrvx3<>T^%9%$b0b~NSBU1MJw}fRF(Gvxsb)5#x}~b3BGgIsw89JicW}UxJYzb zT_;%BYnIl9_Cck$h7IsMJ3PdrWM7ie$5>{@np)06`nu57L1f5iQ$!t`@$kC7|J-b^ z_LHrxoOsL}`s0Jx1iF)27Cf)E>12_IkXz1=f~;fk(({^Zq8xhF&l~ur-+c2<`|5Eu zNR47qSgvJZdWIAZu(je3ev4HW*wKm^-hTRSej><(BC(*x$O70a=yxt5BOR8SZeM6C zjhvc>#3tBQw#LkpZD)w3gEyVqV+)Ik4fkgVh9sVR-6f%5ZGaE#GF+#(&~tk)wKXLR zajk@5;4ns}tg?KCbqt{#-Fmcm;8Zo~YhG*|iJ^Od1lGtWNK3ZcZ!ur$C1YU#^j~Zr zh8{#8eB3I}{~TuOIjdxrh2y;?wfdSmE;bvdYeYTRslLgtCMOSo-PMrGWa1SevT!v% zmLL}Zl0o2pQa34$ib2NBt(@?hQwy>*6rTB8OhS$o3OF%g7p3Dq9_a;gk$3vuxx6+V zb)D;8%6I3#uNAgg{gIdbYi&!xY1Ol5BaK|b$%9912bj}Umzx48CeQ(OFZoR3qA5Su zPSUy|cmM(HDAtUrEZ+w&wy4IZ*}@ThvBZ2r3Bw<%x#b4Ng~zJ&sASgymSTM;VVOP} zkb2#x-!>9c@0&iab6%^^kt)YKQ7lgE=>}O{q@a+z`1&$`V3ypLXAraJKy7_c%A|`= zPzy7NNnY<5s6TNjeN3KIO`&Fhjw}~k8N)4|fvl5NQU_)o$_`aGwEGGQNbJ5s$>R1r zRu3J^F)r5|V8%_9VVpOz5}a9a*^<5{1it^njfb7_a^q>@#BSn(z8FulEO`Cw9F2|7 z>uS_=6i=#YHo>BG-kAGPe2FowBL_wo+$jh8R|cKUQd&*h>J`Rza?)|91&-ggKclsooQ@E?-&D%T*Xjr8IYQ?t zsXSJg3eS$+qdnD{%=Unb?4;SAVUMXkg40ePb|g~}?Y1|Cdu@?5R;~PqB+{SJZk>+Z z#img*_i;&pWnQo6WkhsIQ}m1}uBS{NFqzvZ@P|-AWpJu<8ZIW_Q7C0U77*)ta5}y| z8gN~~A~(|&Cog#QW(wnk6wKsue*S|ViUS7KQCnSA#Q-V@QT65>(;QO&c&a%HY0&5Xpw6#8A~gI_ zb9_i^jE`qVlMe)*92wH$)!-l25||<69u^wX8K1x}%QZz4Xj^09Jpw-*d>b!mV?;$a}F_D$mlow$tE?_a4Im0JYvvcfIc9_3Ir z1>?iUzOKv$`wlso8Kwp*tQ~zFi^XN0Lzo#B7;n9E+`(GL2C}_1G{``QlQjDWjvOI8 zljmSc%%%%iol>*N-b24O>{e*i5)K3h_{r=Co0;+N^)|*2Kn4 z*?7I1nS%2Q)2OVgMm6DFpd24$d1(C78nz?tRNTgVXpVhWas<3ZB14dGYUI(q8O}4H zSR%Pjm6WAK!V{-onKv2hovEqIZz!$eBUl(kQ0yngx+=^LH_sZ(n24-#ZudJ;yg?C@`XIYGGx@)A7Naf}}PW=pW z>2*i>>mvLAfP825q$gLBgUD#y%PNZ1J#t**OSOc~$(LO^<57;RFtvebtH&#s;&i@# zM$B%y_9bomfPnm(yObfRwg(HQwC4Cc4Qd&ayzxnl%|@9er8fH95C{GU?W-4*&e=`{ z9Al2dCVK|z89KxQ7vcdypybG7RS4|7I(V+R=r~5ZY3}g5q}vnB|D8l(^z&Rw*Z_d+ z&G~Z@xj0&TS$RFav3zzIve-RnWn%+kzMF~wj0Ph!?|D_K<&PSsZUYh$8Rtn7>7_w* z31pVf)+d`;9hV$4V;vC-px6YXJX19yiq4bb#~<9c%|ey0F&}S}ZW*lXT!){$yQi-v zJ)~Jdx4)O;eI|W>a}QPjM(vU*|3nCHL)b|$n>FIe_+5}v5F27%)!=oA5fd}Uw@y7eiiZ3yGE zQk(?o;tJ7&tcz`V$apEKA#^i(aYl)Qi=q4&366WF%MN+C#32$sF3Ou8!;bF{f(E9d zpNtZu6<%D)Zm=kKa#dr!5Mz#S?-rHWEU#ldR!&wM7hqi4k2_^{1~nb~@-)e#I^jSFqJ28Ux`VtubZ zjSSeE;}lcQeR6l2xmhm$l}cCuE!~Iqa!}qVjJ(oEO16qpfpN6(FW5xG`!5E&1tD8lvLLhzSU_xD zKQwG4U;YlF7)#ueZ3%Dh5skW+H#&%!EN35c;L4zv4z|Y(O6>K{q@^0r;6pATwYyp|b}0N5vpP}~x| zmo#y1YDxxw-Cyi6T3JZk*-BqoX+{516ORxsM*&LuPX1nj^Fd#IG&y+`o(AK~=^&gUX}fC%_K+qXQl-F3XThW=m0uCRY;fC)ba^JWGJ~RTiht;m|@@1B}@h#Qt0~@7f zvxl^C!P(kJ8FV7;2~Xp%Bkz7+@*Ya+IE;PSTeH%2^eUv)@i{-6^yohJ)%Rs95|rbd zys)QBTbNB7r*z1VCFz3c_Eg^M&037)&V6u)HCcr@TLM78a%SsfM6Fmqx>qLVHB?MM z%xtcy$=9TU5>mm=!GF}4X8h&W*t%gm{Ybw$=CM(7J{{%JETUhco)s*kt@}7ENkv0s z^2`|%62)MtGQ`WVn{qRBoaQ6}3tZ%B5hn<$0?kQ;56UrZWMf20u*-{sakj^kjj`(C z<~N4Vmz>&!o&<3^$O7OZS5g+KN6c|pb#?qbqS$bbEF2@U+q!v}ceK^REcIH?XeD)Y zrjMs1>J-hYBWSLHr!9woZ2LQnWW_QW4>R9}u2Hr6PlBs9&AigkyCZ_(1^%~0;o2_$?HJbEwE~^fu*aKyWR&lS>Gc~NF{7% zTEaf>!U%E#C;jhMJKJeG*@XcB6x?Vw$<0>#*Ppnz*CW9|M-Re7JG;-;maCSQhiMWn zgHM40PbGxkus3iE)v(v`>&XM&Xt07)Ia0Ap`#nw>El;y5gTx($ML-e4!XR#I!-L$E z)J$^+$ny0IVxZ}=&K3I)*MW5WbONIsPd^G1{gcBU)|o2O>2`71>dfYJ z@8Ki#y4h=!bnfkYLB+)fZI5dd!o|bZC1Fk?>rTs}oG+hgJP_}ap+`Bjv^;QsW?-aS zind6GiwwYc8)@VQlTUp`mjzKs&(3*oDg~6Jnj{5m}FMnBn)%x)LDs-0+Iz z`&kZ|fWj(+tNeGPBTYuG6Bk#ZL-myXhPVPiU(p{&bh!_LJUexEE4iw^G`U2b&H3KG zYM-crAHbFmcvfC7Uc-dDVTtMybpGs0PXWDV_i#wl?W?lb}_A{)*@3h~H?61K8Zbg5!Fn`m+ zZ^rbW#DCh4UqqIhzWmLM{FC_4#q^69L;ByY`fF+FYGU8ywg3R!n(xwb|Hq^2JUzwr$(ah8x?qZQJMfKUe4K%v4WJ&)wTq^K^IB z%x^hx2n-OI|E6)Ph#<)SGvNQ*n;5wm0qrdqJ#2wjIvdIx+&JAgDyB|~z==dN3ot?? zhKXOAyi>m{ZdfB3&@uCt6ARYL4Bk*+wov4t#99UrhZ`RA0AJ8&3x@}B+<2Sw*-h*` z1lr`z-1!lCQ1tCY1W53xL~R3cQpA*>)5N2tutEfkA)~{gj_&SUWFj5;Q)&7CBAb z!6veadEMFFjgG$ptP{(a6%h?%PH4(xk=eCxTy@Z!Gg_2LV3~K&u$lIxQPc+L8<#>6 zt7KF>on`7hbx^pd69YyMocr#3^>)8$by@HLfYLCrL^A+YzoE<^nzgQKoG@)_sw~)y z2$73|Wu`LjbZt`P(^=v>*S~P&u-<{#0}&uHw9R{>nD787{B=Z}_5OkP{_vb(lcPF^ zV6F_Aaf(bqB2}g?L$``SwNAs7I(m`1O>1gHbL66u?s!>aEN3GN$n4+h9NE<7k(cJy z@g6%_4}L)fbRN3k%lhRo+Q)a<65tpY8m4O5h9~0*!j#aLUVQk43$U19W+6OLyzB%x zCVH2m$MpA+&uE_;*2OCuGlMdyXI{5#B-!cvL|Ns{nAB84V#7M{!)J1W{br$8AKWS0 z_g39ZDlaGAHYJ#{l27_^*8Edmo{82_S-zNLq-n=w1R)Tj)}z%eVEnvXd#z+tSlu3y zACz!Xp)rCO)~cS?9L~aL+gzGO{>yFKUMyl*kMAX^vJ8! zdQD^AH_o1^3;G>y?>0E1>T@L`gm9wE8u^dxfPoOe*TUEcBH98~3-@GU&7}nZI<-Uy zNX{#W5U{ASp&pCFyFXC4=d)(hR`>ez2UX%RQ(Oags>)|LjR9nZwdQCpmM&!vjW&9L z9hIJDKG4q`HK$bjIB1pCc6ar9VL6F^c*FD2bl?|X?RaQet5e8mVb7oJF%gn^co#hX z8qa8bTqKf~1b7gLbI-|I<4=LMD|d$r<%=(4(7UG=dA_09FIw@Z8G8JiTmEb@Px{ic zQQa9g;6$(;-@H-KyC97~XB)rDj;4#ep)m}wQ*S5JL9rgGef8t{x^d=&nxfF4KKRfn z6)UQIiLE?)tsA7U5MF#gS$r;yKV>Xd^XYt_X(2y+9?5}#16^n%N*^o1P^jh-%qQOu#CH77lIvB$t zLt_CQRRNNh9rXJ%M?sMD!lMsanj#1|`rK^4sR$VIT5S|V$8G+R(dogggt=ish)HKI zJaFdkpP7cae%Irrw&|>|k~jK^43B>jXPx&3aFA3!cv#cc8+*Y`wJ^LC_p&PHjDhE*(xsF=mI{P}*?T3lw1= zMc?A0?mg zQdKu{(Ip($YPBE~Uau>4*n*bFUc-rN!kp`|1aZ~{uub)+fl!w9{ik2@^6bAL@Kw%8 z>w4PT*Dl~azyqL!*-c9mLpj&tCxMzfj0i5L8JfI;)`KcCTql3O7t*|H-ESqow=5QAVD z*fx(DKs@ou?Eo&@;&L{ULAleG!$$rM7+|cwarC_xAv%g!rmsm4uk-m*@o+9HxpGhh`%iYwVfB~$=}-$%H2YDz{9WZW%yOOQ{k{25iFD&`Qn1a_aizkFMF zR?w{7Zk7>+<>rp{nmb4uIOX}YcR17!?!1v6JX;85<8D&UZeXCpLPuWK*f{1e`CR7X z%qNh!k5axBviyGjUf-#=>@b#HFYLW*+lg(+_Ps_Bs4tq86@dxA@z zRzxcL8^>!-oX1e1-xXh9p$&mJ6-dT1)igiGly+0p#d(IQkaxW7CLdqTjy#E5C)=~z zZ)X;igt-jh<1d?p^-4Z5Zt`!-^Bxn!K7B=b@D6v}d&vhg4>y@tientf`7Q#glM@^_F{bRkXGX^cxL zxMj_zuB+Q0&8OqLY)1q(MM%8eda2UBXs-QeS6!Q)ZaYE#r#7gwgc1yZPAp3{Bz@KW z)#Ai&74JZerWB)!;lXkSSjE^@CR{-?=`dJ+XZtI0HN;jm@?`3&M95xAT}qw7wD{fm zmx0r^zEJ`iI$x&y5P>4T2FR4#!Qe7iHMRu{B?;G&80;fguOvA;r3+MvN2U{k$vS$x?uv{q9H&6D^-g;8YF)x zLZ4~|?!Rbn9&`Y-gvL5ymVnX(??vJ;3TwQh&$_Lip|m$u$!ZvAyu8&}L(OU;&$E!a z>Md=679m$lh-hSC=Y@PwM?A&&By`(VVdld;X>8Z=mTPL~O*hTfgBg#d2Uy4$%{;{ol)cdTeU!}Vv@ z`ZeG3mBc(wWMHIV4}JDlX8V@r4|G;Q=uCJhk|3mx$iPZH4|JVTY7nKp)2YrY<1^=U zy}R%&BlbUN+a2gW=vU8Y($&||Y&0rRaw6vY60<|O$?pEOyQyIfZq)U~phhj^?&IT) zVNjiZzD@M{9i7!1mJd{m8Mf`?{46_`ZF3=`a@oRnwmLMWuMiE}!gsp54)G_hY=IH_ zUbhN_rdt%KgAutVL1fnLJwgH|pRlc0@rWFHv#$9mBA1O2I%-iOV4G<$h}pk^UkOfH zOc`Z-%Zr2;q+FX|?<(#3jHPKI-|V#-k!KVILOB6?_k0tW@b@ zLt4XVx%E-8>{6i4E|Sta4@D3P%3Od4lqkQ?(mWRTr9G5Z`M0evYa0T~m4m4;dabddy^FB5EgjCeG;pmC~;;{Z%IeiU44M|7B%$7s) zuWF;xnv4wWf6USSg^8`UT&P)k4i`xwyHa}3t6EVk%b{jnZmTD|Qgr{GysYF3URkY> z!cg8wq;_6@SrnOfPas1}msa^JwO&`bQ29_h;x_VLZGo0ir(v%-%NrA^5MKu0Ub&|t zKMS^_Js&ZU6Yd<7-QPx?_exqs+7%c?5u9L)KNicZ9|n!2d~^-bQMn%gUU3`{6sV>< zAO?~W=~A8i;TX~LU`DXp=Z|3L@#!5R(Jm@6cf&lSw7fU1#*Jc})8!!*U0UL$Fn`8OicqVMe{#kULu&IA$gO!YmVJ3p@R=6I;I zC>p>n$`-URDT2MlXU~50R>vVeuT8dzP-2pdr-P5zHwa3#I5Qyd_Pg565{|jrxd8Yw zFP5QfMs|S~kn72WQGM0@L4*q&;I9yj$~7+NuL`{!b&pRqhiB%9x1~Dz3XvUV)U4?1 zmfydOYE~QDHL9pq*Tc*5_vs@PazU9y8LpJ6>=T4)bX_8AmhVrAe8D1;NVAoJYe?36 zO8Fxo+4BrRtOT5}sF5@&rnD)ttn|%?>29f2%M4&jWZ{WfuAgJfDQX2BdMnsUK^ z&9!v!H)u!JH%OGtHauXW1V>tYvTV~#2z#r2V_mxkT4ovv$*So_+@kAq8i6OA%Q+^U z%?)}@KrWCc{t>sZuN)I^u%;Df;EnsL;LJGCVCBy5v+wza>e7GxhNbYudyRY< zcjcQK4;*3%k&j?;F0)6+WN_$nCdX*}c`&6`t^9ci$JzXa&|4dt{Dttxo$IU6E_0#x z9~{H=K=$Da{=vLPIl^~Byvh~)i`MDN#{UP#t`S&JCG5kJ z^`lM5jL|cMWp402&9236K_1jME_L2etsV0#L3keY6FiA8>Uf&JAMRou$PaCDozcoL zh&T9P)2sp3AP@Bp#5cp~jqUDEz`Y*A-e1qw5DK_j$R^r7+2Z}$!@54H?W)Bk2HrY} z{4q0Ue!r_~l%0yIBydLt9wAzqt#+Fk0eAf_^Cw0^y*WKqJ&?@rGtfwZ?(oQP7LH1~ zs)aLEuZ1sV6FY{Q&Lo3W$EX4y*BD)oClV`W(iQL_-)WGUsuAXy4k+Oou`L~YtSo4O%YZADZh zVwz{Gz=|5+dIYIUrhb&1Oi1%DHM76@997(U28&y(*ABEWa)3}kv;)GO4q7m5i@PwN zjQa78y>w!s9mvZl{v9^72!4YX48&?_lNMytl=(-oh!!c(Sy+~!9f<^gN<*If;i*z5 z`I@@6?dTutXo^h%34DN4NCiN>6#5*|JI9AYF!z#2ywoDEvu_&fM}DqmXuhI74g)hv z5F=gNc1=(T=A&Lj9qVF+Kxa)fg)I2zcwvxL*I*{E>1Qk~f-bVt-GSMJ#e}}*B+YH% zTw=P&e>QC@W9A7o>F>7kS5%b!9#&dtbm^1hOro~KrWbDog&W&E>*A3TdDnO1Zgeei zqlGN)EPCkkOSx^?dFo;r6m;Lnb;di@V2g^J5JsFVm!>kG2062kU(dqR(fx6V$YGAh zEc6+J0U{d0YgYDyc`igypTG4qBW!5qXC+S<00&kIxcn!elxGp>m~V40l?jsvJ&yqc zfz^d`>D+aqawP0rFM}%ROD-MX*F)FeR3}kFOS|;D0*in*Ww}h5Fom=Dept2AJyInc z_EWX|{vSG+58rH}71gY=64QaJs-UgZD8+P@Pe}7KFJ?-hI(&!ntSh7*MY+x_BE%C+ z2drCa?_0~pAvKscVBA@jBGQz;LP@+{zDT-n5KDqq{ZdK1;XIrSzm%C5+UQ`*S$aqF9qm78Zv6JF%V|QCL==6WFa5DpGoEMuVOPhC4Me ztgy*F3iMnnj>v+@@Ck_GB>=<*My)GJeO{^b>m{Rmuz_uf>C2KJH0rUv!lAS}+h(F3$-n2l>2d=jAK(D0Sc2MhNSXtU$FFldL#Xlq?6Z^Y5$>W$3SCkccShs0#J}J1T z*&a5b%-Fb~e-t%h2pKGOlQu}{VGcp|XtCu(S6R(RxLFCTO&z=!J#kjx6~0|8sd#si zFEC!R~_cxy!EL74rn&D)zx&6k^Rzpz{l zjj6St4G%yUP_X`WId_8DXKE_moRce~{a?@63Iw2$;&uFY`s;u~JcS)Dg3`sFOfPo0 zW~T8kQ1}-(-xEfS2X8mrLwu$7++%VCdZP_? z30g&>J%S=yw#4!T^;(_I-25sCS9oozQ>Z{Y>T;>s7|1P%1J+R%Otoz&p1wvhxV08U z_lrSBxRPKgENhxLdg?@ieG>sp;){mJhH%Q$KufU7PuFgiO?!LRP6lLL0R31of~nxQ zVE*U`n18aFd~%HNy1>OoKj_>@{-vWU+Mg#ZGhl0#1vF-qvmV2BALu_w{}-sHkLUTd zUy)oP>4QO`W^N!%7;d}O-mAl20qbib{Ipsq5|$6Og8?-`)P<8P8qalTIsGpsJjfb7 zPD&8W#B9w#N8tkOZqk#_K-d0@RHCG#S*#G&X-}Ajyr~(BC%JVRk6IzC)=JH)0Fw0@ zgXYyCJ+NSu%`J|$&dyE>0EGJ+Kobns?=~P5Sr@#3qOB7WXY=S12oFhg59)by?N-*b z(X}zX$>Uv`Kb^cmdyjv#%HrKxe;4Hb;a!$mD&D3*@eyGM>F0TtmyN>~SmIaoGf*-6 z=Vurl@wMlXc;+VSgGR5EzR5EYmL9Q8r6t;hW#59QWwk3|PQ#m1c9kKsv7Zb$owRTJVjPjLy_(_ZF!Ne7DK=2)!_(F)D zvmek`)vCVs9jwakwL(4p>?&}-ji%D}fhC&HC(!3WW=B&IPt@TrxVD#2qIp%aS1iz) z9seoeusX zbI1Z~j-^Hn)-apEA6tx;3Mn9=d8k~~WkE+em{aXAD}(i1YBum(qIy?J8>g*ejPijB z=$}(6<``FMhlxu?zO#gs%m=lrlT%w(S&C7u;Z|chREl|0btYfpP0HsjD`&j5?vhXO zFFl7|>@5pjDo;?13EayXAAPl;Q;?UwU0C*jZuEael%OdhYKJ_0C>Si-?31 zd|hhPY30Fzw-?jDrUV-EY$cD0hd;}mH+)kHyJT6oLR3Zx%|29Cv1FMU7N#**E+}k? zIa?0;yBry#1E0E?_IJ4k4xTfKITU8$TmJ+MYS8a+tfCb(k%{i0L_15&4ACkaeoLS| z6ttKKB?oZ1R4Xf>>sCHYBk?G$mbh$Ik-ojz{&&4VFLn>^<(9LpZhyU^RU3 zmE?=AMlN?y@Y;35(L6Nji-Qx|Pp_X|vZP29M{^rBHqvd0lZcTJ=OV~cR5(s|$ikH7*bLlu*>Dw<;fs4k_Y=blSkQ=s z%pg?qoLs@6G?py-rUyqPqcvC^^uQOT0sw*$=#q3Q#dJOFK%?`4)T%n;U}`{sLM#G; z?lcao#|1_$w5;@Cgy}HX`Xtz)uv>AAU+EP6Cw zP<)gB{^&R*L^}mz3F^m$yf37()P;1@#<%jjpK|=uZJ5S)V^q?22c606>D?8{sBg!e zD5&Ij4;|!pF}HCs4bv0d1>UDUBHfV`iJg9loEE=#5wOImG&@(R_74x&J649uIsR7s z-96&KhGUuuded&|?vqq_l=c*wM_Or#nw=zW&MPGnT1%;S#n}>{uc7C5>pY%9T zM2B^^Wxw0iqN4&gUvQHBwb;a_nSX0CofJJyt8FqTgq<&r>@NJrFH_1<~wVyQ^ZCcSiR&8$%tl~d`_+E?N zsx$w`dY82obi;l7nQ9$4Q2CqlCs@x%km z|F1kvlW0hz8PDaXN}z#wH^%E!E4-9+Kprgd>1WSPv{?)q#7}W#MtzmPZ^lno7kW)` zihSB6+&Hmmo{6bbaQqvEq+7&T4F5kV%O+Rc7BelLQa;>;2PG%3>3`?cFD_I%UWi#< zMx_y1oC<+*K@$kNUj6ZJV3g5)W||B*>U^KHu^^3g&A28tY<%2WG>PW+W!x#z&*4w&+byh13La(pM@ zEo$L+29Ann@D%jla(UWnMazPJ1DdZut=%)%$@8y!L0zdJ7IU0)rG#T}DV z$~&R5lQcuxv%3NhPAjvMPuVh>vY=Fp0=AXQk&ui{V`ogK?eNrMB-!}Thv(GNC-1E0 z?NVZ?*Q%^QlX#X;q`Za08kjrarUvim31%dv1)%R9bdf65$jAoaNwteb&nZwCLM(B; z7_fI80X%dX-x(n(2AJKsjk5fxo{+Nig0j`$cp^69&EGZ>9^lIn5NN21b6x=)+a###GiRv6>1cI;ivV`gwMvo zKHLoka7e-qDC7iaZQfxaE8wj+<-K2gluqXJ9MHsQ>!<|^7}|8(_nL9D&`E-^q>AD9 zqiP7MwPfp{sizq0v>K@HPtlVx{84Nty?=5>GT_UZ3VmQNym7DS%IjpOY#Q4bm+VLP zDP8RLqBcXP2^Zbxy2F^9h_9f_iQ(*%XHQWF<5&*S!(pFPn)8fCPb_uy}8n54}(S2!EkoDdOC-tuMIN`|_S-PtV@vpB`D2#zI3r0h>=w)hrp;AAZlQ zu$+Stf-??gNO(zTN3+TOW|Hv6g?bll@_b-Nflzu)FDYme4;t*r&kso55$i(+A!SA5 zv5=oeS{;T8hJ~8L+vq>cHZ_C=OL)6R81j1V;%agh6v%N|?~-&tZ_uh{E328c9le?* z9Q@^g?ZnjDhd~yeL`PX(d#tAxsWI1?s#fUg)c_qIasV~sWHaeSGq5&V z2|BW_kbFl7faCDj|FjL8r?fpmexR>t0cxIoTheA-Vc%+A7jo_A)?DLMT36|;zp|UW zpU^*6HOtD7^%WXsxLyH`cU}oy9XR7|hQU5#?CXsPt)PO#W&T@`@1DIPP=lomoIUfK z07DJXHcD9PW>HXl&v|MOO!%~bvAcDVxjaCePXrYPLJzYU_Gpwl!b%u+E zuE22IZ1p`gjmwf6i45+DJgX-sIzh)|+2D3_mhZ1Ki@{lB%ec#EwgpF61ksM}L|3HO z$rKmNFm-`9TKd+TfZsHM9=>QXRUd+xHrA44wGUAS42&kWM{9$X%29dbAD4nRlY?q^ zwF>IzAt@rOh!Xjdqfz)HPnGL|wXVERZ}2(Xex5vz$M@^?glTF<#5Mhb>LEVM+?P!j z1A?N|I`eNmPv_R{m;3XlHTH*VGft35!BZ;F8~$OlM8Bj?W7IK-L|81yxU^{V!@SK!eIDG9=hv=2SIH#1&G0m!tE15} z>vcb>3nlk#N$c6s=odQj;?V&zWo~f?CM|eFT+eOyR$SUzI%J;z`?sU zgseTd?L4@xJh*K=xNSVR^A7hW*pbr~_@jr%Hwii{(EFp0(SmU=%%OW^)33sUUbq<0VfQ;XKOm2$eq z0mJakweuKkBackV!)5>;%t~y5EZowNm;#*mgz8qLP@Le3{4y{3&}?W`wpB^+M@QN2 zYHhZPWHNJE2cYnnK7jO(jUO>t#03uGU4zrUJeLZ@P+5^r7X@)t_gZphz*mm8*)#MofEzk zC-E5CY0Ld_P4d|3mGW!rTx(-FlH9H>L7)t!X+o8!S!To*j>d|Dv&|HRVCY}MCvLrf zXkEd&JD6ZdxMU7xu_7!va@gidOo20(f=UUm)>`yXPtq}QX%~i!z8f|k#zsSns3TLH zPn9+rv?@Ia75WDptifeGejF{(-=oYZ##Kh_#Md&y@UD!&reG7Io?4-ejm|~guuJ37 zqT}gpP!={K>!l`GddRZh2)#SCL~=DQ8PK(dL6TBH~iO!ga2nf=tRPKras_I z#4AD9JRcw3Yv5@-x5&=d*BUim{q47_r@aonxJ-ZKJ&JFs&*s%$QDN(_8R306e+5&) z^|09GI{58<3=6QGaPL+0YwdNGrj!3uer*GBdkQW=Zx z&{3QuV?f?BVVU+f=JQZp&wgdhN8Mdh0Qkg;d%$APJQ6%yOS6?Xn_qj}!M3g2BUO)F ze$eW2iFy92R$l`tlz3$rbd_KBRh(ry1z9Cm6MN>ue7J)Y>z?W-L-?~t{PRWp14krK z-9l&omm$7}OXo{=`LW?WqPqQiCA+?3_#C0Ct7f}#N9Q8luCuhcsc`O^#L8fK=$xnO zBAWNttksvUn1#EHnhk6{h#Rd)a-fr!xCA3V7Dp19B;3~5V%cwd|300v%V155}wUYv4q*>jxI zViov427W=rH^$6qFX-Y<-53PoX-yE6vT=H8B0FAXi-LAGG5ghJK}Yv z7;7@46{B2+QYNwJ+MM?2TzodxLo&Qgg|O$GQvw`hO>(*BLV+1A;))R_?E*6pQ@P}Q z2TM}pnc&@LK4&{~`93y7*nQ5$9qRh9f)umhdivvNn(G4-KJ&UCukoLJL-Cn)7h(9W z1c@l3VpO`Z!h)X8$hRovxgD@0%li5_?;1-5Xxp)IejO_$o~@yx>xvV$rGM4sz2_lv z&44Xg_X*|;#bj%e<>(?gi!(Rt%_Ik_0AuMd41kk^b#*`MS|MQ~kwhaAyjgSY;(^Zl zdGMb38A0kC(bN0lGVQwT)P4TCHQU)?>oW5)5R69-x>phw6lBVSI9l-kGn^v$GJ$>a zmI;2~-JSlEV$d!4^W&E@u*sd2H-kAH)^j~zZ1xx)u?Aaudkc}2D$6WWD3dq}$T9{D zJ%$!%1||;kY^l=YmO3Yng#(wJ<#jpIGCafIU?9Uo;G+D8{L#=#0Dl1eJFi3Cv|9h< z3*ibf#bf%l4GDKG(4GKz((H+r^FBZQ`*na?GWH}(rB%3pP?qF#zxH_}kQmHkur4An zE?6R;(u-FxzA3@YpU?>nmq|`YRU#1BjDt}{mc`bMlHW|i4`9&4li6b+>ggl(X!fCdTUtU(5}&h!ANPOVzDM=o<$l&-$&9iAVfXBjH; z4xG6uuQ<0;GhgC;g<#)7(2$^B(q85WR5VaDQnaFVqv#<=G<29GKq7L} zI$IiDAdk7WX}-+Z<~*CTS~jU1D=bT^j=|dQPBUnqRD&~HeA@FE#MT!Y?7cA zHK`@as+a*;7?tXt%Ql9Fo@UiBK8{Cs(UE{yYF`M%9p@a6-PhUG+Z534+gkY|1VMKE zVcjI(?7`fD`U&!eeqp?hxPHg*2=$(E=xMlj(C%|oC;COFIH2y;RwTe$y+?$re$}Vs zZK_GJK&!PJZ>kfNXyPO0RCDvsIFUc}h^;6-g-kuN2G|zdS3)f)Mk7X}p$l3&SkqrK zT+?ul&RCE1VlhxNV*C3K{B>QMhU>soFV?2|Kv(ESX&d1m!d5YFk3xY^a)Hno zB7(mo^=gvUXolAD6{Mjt+B=?0022g!i(vqrO11n&s09vx6sraG12udkcAs1uU0YR~ zO~;<=F4T4Dp52}u-8Se>{};MDvWCmRM=$uM*uWS3i}c;j?nc|l`T+hFsy+Nmc&in1 z>^k{2rO^5zm5wyyT417~w-}o34gbVNNz97ehNp}fMNEndKK^w58wEPW#P3EWCybN8 z2M2fuc*iK$cu}x^b zh;6=T;q<@wxvUpYQ2Lg-vHcA*>S}S<-#uv@9Vej^>)11bipz&Upa@4(=7%F1 z$mz`)%$Y}21h;UoKy*iP*S`bvKy}A;hjdrt`Uj;uvc5X41Pe;QUr>v@d>Q2pYbi`;&E`nYJAokLE9Z4FZ-)iit88+!xL$1OB7or3(d`Qot zb!!&v_%q$k8l&>$TLmP&R)I`x@7z1h*m6Bz*$B+W;ZH_g?=WoO;Ud8(`{n&CyWEC> zgMbJcG&X>_tBo`Lbdsj<|8BbjZX4KtypuG}8KjJ0*m~iN;se-`UM9Y(>=FG2`2ZkE z6C`ra8zW0a6;0^t{axzczM)(>=C2xpbp_oQZw&(FXp-zC0b-wR9PosgLxR1jhl3om^Q9gjFzqVMuuz7 zgMF;>nc*O!HRgb3OIK(blPe~2UkrJp*2CFGzN zw)cNwSOA#IOj?C`Vqr_ulhG$=s<(mZLRKeeQZ8xQ_;iA>qDb zH^qj3PgQSZ|2V!LrlY#!IQG9+xN)0&3k=$4*U440Q9J(HZT;2r2fA(daqMN<*=cT! zxH{>_vA4pno?hQ+SQ<=}ECLWR){7xb8^Mlt2KbM9JZJG!#{c0zm>=y84&Fb+pA2mX zAS?Zvjp0qNT|JD;sD7|$%<-$jRhCf9&s<$#4$53_Z}XHsiBj^jKfJ1%%O(u#xRp#_ z%u)=f;X(L{pC2@ch}M&I3YaS){G@kFIu$vkm@B)btS4p`HjW=e`5*kRP)u3GjWLX( z2iy8#cL9cxgX|HwXwKS~cRzN^oH=`0e-+BMtN)|xaXFVwcAM6Q%Xo{5TiYZ9)D`cD zSzMk3{w3QNh{tJmJo)ZU$Jj=%pZ~}&yX9+9MHlk2S+-LFDlVG_Mz>q>$06z=%X-Nf@mb`9xyJ&`eoyX1>`zEXM(;YK}sSw=d~>ch@e>42ya95fK!5#9Cfpghps@!g?cQtdpjU#YHh_u#I(_rY&TuM?gk#O{OmsPC>F68a>foK;&(DAe71D^S>|Y%NA%e^7Jrs83Ol!krIOZR8Spt8_l4W<;7P9fC412GtyXx_{s zaixFo?GwY~gzLe5_1Xy|N9d#Z0N!N~EW<>w!lV(%qvK(y5x~U!mC#H8!!q)T>GMbn z^waU_66xeDbq=6yFq&iy^2Q;9nONQ|+CVO4;0kri1o^}iomhz0hd6S&>*yPkecY$q zpeMZqCX~^llV6edM7Exex|U$njbXdg?`Q}SC)IdM;V~YmF=_RA|3I`%0%(4L=!4J< z-mjeQEwL|V>_G)>;GA*Td+l|-)Q?XfG{ip$WK1H^plF@@S<4i|gN|Sv_rr!cssboJ zewJ6#SVx6iR1dqy3HT7E03&~gXqaf}XzFN7F?(6+VZ%YR@PTlRbP#z6fDyo?*svIv z-c1r4o_EZ6ZTQZ3EtE0URyvlnoqeiEL-5@DAmC5_j#RGSzGpxJXSzJEUDIe=L3htk>K@(w4+@-suW)N5LK#T?* zHXbn=F`+V~GNQ_Qr%e4#>xS3o5@=t)V@(<)N_k7j49|zp2hE4gw{sZ6{qlp5_00x+ zpRT8oD8+CNKzmP}FO1GT;#W_tS@Vc)>gxKhj)6BY|`qAs8bVMH@z+Gn!zGuymup2Cx)3pm9T{52yFt8cv=W zpZw$*{TR!252ea$ahipciq@$ua(LA|v>BO?Q8KhxHi-FXxUUxd2=;-4=?8|v#rcb1 zS*(sH8b+}J9YYzW>q{(F_YrimjlgF9bNbVInT5F}AtV8k7?RB;7U{+alJo)G(HQGl{Zl9b%Fz9rsd=>^ z{CfyO4kj6mUPTkB8bsAt)kxK3(RlF;=?Md$84s4-h@B1y+5+}J@x08uEFCUkc98-& zT@dfg@0ds+J)lr5q0FHylCBL=0Yjx?5e*>?F%3}-Wqg+^{Jpnm+=JZ8IHnH*tKAfRZneRy zD@O@FQlhrsx5MUPWf*yDD~CJ**qnY|Ma&2|Na4NVgB~HMwoR)gy~Z-ncg8X;jH#w_ zjSMvG3MCqN*1TQYx3A07NeJ4e^JGz8EmKH5k24hMQY3l1-XX;P#GrZ1aLj@x7{>p7 z7X2;X08x}Ij4RpR4`)b`4_Aa&1XYAor0;H-zZyO#o{w83VMJGtI2$_`aEDnWhuv9i zUo`L9Ao61z)I9`ci`!_1;|F287@Nex$Q=6&&RO8BIPV;}&pw13Nfgr5%cI;QJ#|qH z@4r+09j^`e`@aHH)H-D`bGj92mE^CCb<|=@KjAK8g%9E5c#qEZ?~~V5uEjYWeL<&* z38}85qk#X;q;k3M7x!!%2i{ym4=H8_2uk7IUnOvv=W$c14Vwp;>jGGMHN$kaJuCwU zhR_RSW=;QdRKs9v&;3+9Ghr*!g?EPX%AJbW6Yx&96)IezO7vA$z?dwlSbRkI2>)ki zPQ@F0jES{f)8JhO#1kSZpE6aW`!x*-Dj<|2YPW+y z5@kx_WhqihnF(J*RYNE8xxj+3+D85}UdsTwJiRy3BGH#rjCSN=oSwlhI0K7J7 z9q_k=%@*8E`YLgq{BPYk=&am%xaus8xtB+!R-ocmYP`zJ51Kopkb<}p&IzkZD2>nh zt!geUHCb|h^MIVNe!r|lYDyVcM8O`W0&@fWYz(Z;@*suH@}&#Um)twmYmW0tsx0p= zl)`An+1IoSQ3yo8yRX4FbEuGjaoc;H<|mdIKobFb-G_%SDC+B%oItGvex0pmbJo5b zZ}*`K+j6nQhm=*z%lZ z%GtqNFPsX%p#nV&W|2+|MK;%8Bg{dNy3idrL3J1$5<&?izrZbm#5#{=7`dHzDQ(;a zYHn$ST}RJppXdK{aaK`L0L&hzLun9@Zdh8nq)Qs4Q@XoDa_O$6q#LD?T)J77?u7-U zkwzNi`aRw6-kCFJ&OFS#&cpwIew{)5z~dno4%*{Q8W>8lvpIbxl znvM+PIUQZocr#&B!1NJrj`jJSHg?@!Pn;eT?^Dc2yBjfiZ&}GsBcqj)F{rPj7xQU3 zZpRuihv}Blar=Yh8Lu5`+tT3%?qV6oCTO&j>o9mNU2x|zhLgPJP^3cql$Nh_d1sZ# zo5&j`=mQHYp-*>)pVr9Pgte%$ySj^w?h~(2A^3%guF!QE@tn+`-`?Y7<-G#6*`Bp0 zgiKIUI}F@u@NdhxD=drx=;V;ukOiQ_jA;SX+geXrpxQfn^@suIz?)!596gCo-%_nm zZY|354ROM1NF+~7n)|PMCW0ZfIVon$1=(2Z!>+f#gbdR83xEAU18l2wmC}6wIr*7J z4U_(zpp z){1Xro(naosWm`?>UN9^J@dCjgjKzx?#wVssT${xvn-`DAL|LZH9tzi zHK_v?*amhf`RR5kYZKCZ7k2q{L$KhQ9*I#%Zi#8WeEKr**_0TJ2#mySMIt<6)#Ua+ z;I{CzPXgKR!|lQ}SL-`i(Ag_JF-oIoCpHrJkNE*evCz*p1b z>$2&CO`(qBXCyC^Bm3&gPwElnAo3|L={vwzljGlI^$`^y@^M$`dX2}{g|<(dXzCp3 zy-Z#aoZ?6?p?};~rT#Wsx*M}DS?Ws=XbBMv1uIJPWO|(_jfQV+02G56^V{cQL)qpV zX>|Zc3z=%DML1IkWua+PK=2qZ;&u5oftJRngE}wHFCAW{Qf~l91DTvC+}Kl&isp1@ zbE>aEpmf=HGddI4BCnouBRxKec5Kp=*Pzzdpy~q2Of*x7ne^OpqgOmVR41b&j$1Ev zpLN1muHcpPtkd!rpyUF2a&me`=}X~8gLJ$)Lk$uYS!tlV4L89*(~vDKAyQ7*|Iv6y z_11#DaoNsE)3St#80Tc;KKeuBx9+U-?p2uR1r9$xEMhM=nrr28xH(^qV{94UiE z56@5!didxM?G-1FIqfWJCtZkszSj1po7hF0W%Grv%^lH79 zPtImEneg3@A-9gLdzP#>+f<2P<9o@_)NQu)1{)Cr#{e|Mw}EEZ5p3cnxW_K8r^m-m;zEn6)cOA`x=CI?nQ&6fpG9V*CIhaxLX#slYTxDc+uUp9jK*1kXI61oSIsW2 zc9VvsvvuJvNA`urYaq)Q{bud78}=E3WKa}{{mG8-R)U5HA<@b<&on~a2cy*~y|@RZ zUQvg>D8$dMr#0{`8>qJEE-X9_s5BlhCrHsF6sh}cMwZuL+RUgY2mD>Bhc6i0q5Z9$ z554y%qs)uMy+_+*Z86DA)}2QnmCiNtyAPjiP?#Y}KuJ)=KW3t|GoWz~2_(HkzUk=}s`AC3`?D@=G%VQa#9XV`Op1|( z7qr~VwDZn#NCF&5+0IMUsA(46yGtfHik!8=R$9d zNDJ}0dLO;?2{9Vm?`j$^%44n-PhN=fiWV*@TtoDd-{9WNiac%IKW#nUULO_x6qiW* ziFDwG$QtFY8d!NabbFb&Ru`ymqKl$kAt?OesQc^0-Fe8J!n~e%z#u~kSQq{b(JT1< z>hFE<$+xRx`lUcxCU)x(wbtj5^@fDCXPNYK9>bX|yr&+wg-2qxOrDzwveBc!A>fcqK`OcXV~3`hrf{vPtnI$!>);1>Fxuw;u1+c=$~~>uJPS=Qrq1BzliZd=G48^R<~AYP8ML+{Ld>+{19tl|<)UH$rb1=d{3JvR$q z>o1}KZk9;-b;1ak%w)HyQGtYY#K1Nf-hx`oPcos)H##EN-)tA*v4{NFr?0IudFLNZCi1xK75p6KhzY;oMAUdxtVS)N}Y%v zi(bg(wFRae@-Y4ex#NIuZQ=*QQOH++CKp^b*lLPwedLyuj4%TfB?5}8tO zmAi`;IioV{2l=!H_eV@MC@nAYui|$qb__33Jj|Mwyfyfj&S)*djr=77?jOF;|M6n> zi?R9B<1To*$^9`>W6IE?Ps;A-%v6C!-wU~6qg9H#9hbEh9c!IAnFI=Yf;+w6k#zUt6heT*)-Mhj# zE{($7+$-LuvHNfAT4osGsUv+sb*IuKsYmoFqg)>$Jr7OMeFDYPh zKZA#cquf2(4}3H~`1Eyv zGey{uGs6lh$72f})-v+UVQ}Qk@^>V{W7c}JWDHo%{cc@xp&hAG?Z4#W#nGXO^%&tu((r)@yTAL?#K_BTErGF zUQDO%=yZ==lJxgl{5&lLz7?BXC?tFf|7}fXvKfrYx>&-me$SQd68{s6WY`-T4{flc zJZni1)FULNV4{5Svn(;UV{crzYy=PXn*Ww)iJ5XtJsg?iB4mChNf?xygpN9hh!SSY zpCUlre97m71xSWKM72cY(H@3~i#3v)@VXsgN(6>@;?IBEh*w(pe0^yd7U0M2yAu3t zi%UaxkC_!RwK_LXNOFgfGExEk#*%bOSS2;N?V$-GIIv|SBsOPVS_jM5V~{>9?YI62 z8ff;U~IjlDo?bxmW_~d9Q;G(2#Sxh zGZIllMMmxUJAz8}TEQNu@1nVN=i1FNE`E9${}@$-Eir?3S`{hq0>kM>T*&Y z0mS9H75W(law@WaMd%nJa|XDM?PKFl*%@dvC)TkfL}2zIl#aS~?}u@k1K>qz>owys z5oU9gfMLAQ#L2S+W4GAbT#JRmjmWItm5JxE>(arVkA5$2#2ox-z`B-rTeA*_Z#Aux zLaBr!E|$g1`l;M!M)ooht2SL}8vZK5(1EFxTM4W9uN>(Y1(ru!zgMLcpTFQ1YkJVj zFCopeNFXhIWEK@6I^no>zcoh@%f~|GFf9HuH%?Wcbv^EOJC+fw5_4=eHMQ2 zq{NSHRJsR%w9d$vA;pePdRp-OVzN=t`pY~^6tsIB-=FFxrdrDK=LS2rr=TqQgyH!2 zQ8@TonTCF(JjJmJGM>*e18gNJy@Jw3*&$buuyWru+f z!{NhJ=e$k`ZY3c$1l)RLh@Y2i8buq3AaYPs1xw!Vd;w;w=8(?8SxdR3u|MXkZ?#eT zogd5~ObVq)tkj-n=rL@H9oIh3}KVwmY1H zxkE4((@E4c2gYie2#Ewz=g;2F4LM$b+_5jLeZnZJ40^4f$jtks_C-$!s$CnE{T&C) zMpjh6_|<(OfyE_gQrD#gwP{9v-xCudN790UdsgeDkh~v7a;~mVqbBC7zBQGw&HiLO z$cu7zptpv#@YCxk_83&COeo5GjN9Ss(dG`#Xcs2ZwyVzN4-S5f9x{n1^c1{3PxV3u zx=}_S`E=mMrG#$x7V@lQpzofDDHW=4xnwA|J=85EkCp{KWO>t^W4<5M34HpXI;y?p zV2rpTNOAn!mAoBz26!K9evM_FLO?cnV25Sok42rD_^xeWj-)Jj3#_=e5}x(t-L1Kf zyo-@yenP_-s|)QjJ!z?R?odi(j>ld^QHK#Dsc2g;D&G?l>v+njrRFUxLzDpr3@}sE1EH2C>#_TM{Iev}Bl9 z8!G>B3Cl37bpYg$xIV8KE*Ty*)V$3}l0s*z9K=ZiTB6M91m%>1 z-)HtK4z985S$3d7lt7Me7{k%x9x|AM(<%C~+)(DH@m2omttBZFhIo;D869g`->t~` zQ4Q#;IAkq-@>P5`I!@yBnpNwqmt{VD0;=Yf=ttigiu6bZ;&~I$S%q2%HzR_2v + 4.0.0 + platform.clients.fp10.libraries + Alternativa3D + swc + 8.5.0.0-SNAPSHOT + + platform.clients.fp11.tools.maven + BasePom + 2.11.1.0 + + + + scm:svn:https://svndev.alternativaplatform.com/platform/clients/fp11/libraries/Alternativa3D/trunk/ + + + + + platform.client + A3DModelsBase + 0.0.1.0 + swc + merged + + + platform.clients.fp10 + OSGiBase + 2.0.2.0 + swc + merged + + + + + + + + + + platform.clients.fp10.libraries + AlternativaProtocol + 2.0.15.0 + swc + merged + + + platform.clients.fp10.libraries + ProtocolTypes + 1.0.1.0 + swc + merged + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f860c34 --- /dev/null +++ b/pom.xml @@ -0,0 +1,57 @@ + + 4.0.0 + platform.clients.fp11.libraries + Alternativa3D + swc + 8.28.0-SNAPSHOT + + platform.clients.fp11.tools.maven + BasePom + 2.58.0 + + + + scm:svn:https://svndev.alternativaplatform.com/platform/clients/fp11/libraries/Alternativa3D/trunk/ + + + + + + + platform.client.formats + A3DModelsBase + 2.5.2 + swc + external + + + platform.clients.fp10.libraries + AlternativaProtocol + 2.53.0 + swc + external + + + + + + + platform.client.formats + A3DModelsBase + swc + external + + + platform.clients.fp10 + OSGiBase + swc + external + + + platform.clients.fp10.libraries + AlternativaProtocol + swc + external + + + diff --git a/src/alternativa/Alternativa3D.as b/src/alternativa/Alternativa3D.as new file mode 100644 index 0000000..1a4e40e --- /dev/null +++ b/src/alternativa/Alternativa3D.as @@ -0,0 +1,24 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa { + + /** + * Class contains information about version of a library. + * Also used for integration of a library to development tool of Adobe Flash. + */ + public class Alternativa3D { + + /** + * Library version in the format: generation.feature-version.fix-version. + */ + public static const version:String = "8.27.0"; + } +} diff --git a/src/alternativa/engine3d/alternativa3d.as b/src/alternativa/engine3d/alternativa3d.as new file mode 100644 index 0000000..3daceb2 --- /dev/null +++ b/src/alternativa/engine3d/alternativa3d.as @@ -0,0 +1,13 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d { + public namespace alternativa3d = "http://alternativaplatform.com/en/alternativa3d"; +} diff --git a/src/alternativa/engine3d/animation/AnimationClip.as b/src/alternativa/engine3d/animation/AnimationClip.as new file mode 100644 index 0000000..4cc5259 --- /dev/null +++ b/src/alternativa/engine3d/animation/AnimationClip.as @@ -0,0 +1,495 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.animation.keys.Track; + import alternativa.engine3d.core.Object3D; + + use namespace alternativa3d; + + /** + * + * Plays complex animation which consists of a set of animation tracks. + * Every animated property of every model's element presented by separated track. + * Track is somewhat similar to separate animated layer in Flash, but the layer stores animation of all properties for some element at once. + * In opposite, track stores animation for every property (for an example, separate track for a scale and separate track for a coordinates). + * Track also can contain keyframes in arbitrary positions. Frames, contained between keyframes, are linearly interpolated + * (i.e. behave themselves like a timeline frames in flash, for which motion twin was created ). + * Animation clip connects each track with a specific object. + * Animation clip stores information about animation for the whole model, i.e. for any element state at any time moment. + * Animation works handled by AnimationController. + * + * @see alternativa.engine3d.animation.keys.Track + * @see alternativa.engine3d.animation.keys.TransformTrack + * @see alternativa.engine3d.animation.keys.NumberTrack + * @see alternativa.engine3d.animation.AnimationController + */ + public class AnimationClip extends AnimationNode { + + /** + * @private + */ + alternativa3d var _objects:Array; + + /** + * Name of the animation clip. + */ + public var name:String; + + /** + * Defines if animation should be repeated. + */ + public var loop:Boolean = true; + + /** + * Length of animation in seconds. If length of any animation track is changed, updateLength() + * method should be called to recalculate the length of the clip. + * + * @see #updateLength() + */ + public var length:Number = 0; + /** + * Handles the active animation execution. Plays animation if value is true. + * + * @see AnimationNode#isActive + */ + public var animated:Boolean = true; + + /** + * @private + * Current value of time. + */ + private var _time:Number = 0; + + /** + * @private + */ + private var _numTracks:int = 0; + /** + * @private + */ + private var _tracks:Vector. = new Vector.(); + + /** + * @private + */ + private var _notifiersList:AnimationNotify; + + /** + * Creates a AnimationClip object. + * + * @param name name of the clip + */ + public function AnimationClip(name:String = null) { + this.name = name; + } + + /** + * The list of animated objects. Animation tracks are bound to the objects by object names. + * + * @see Track#object + */ + public function get objects():Array { + return (_objects == null) ? null : [].concat(_objects); + } + + /** + * @private + */ + public function set objects(value:Array):void { + updateObjects(_objects, controller, value, controller); + _objects = (value == null) ? null : [].concat(value); + } + + /** + * @private + */ + override alternativa3d function setController(value:AnimationController):void { + updateObjects(_objects, controller, _objects, value); + this.controller = value; + } + + /** + * @private + */ + private function addObject(object:Object):void { + if (_objects == null) { + _objects = [object]; + } else { + _objects.push(object); + } + if (controller != null) { + controller.addObject(object); + } + } + + /** + * @private + */ + private function updateObjects(oldObjects:Array, oldController:AnimationController, newObjects:Array, newController:AnimationController):void { + var i:int, count:int; + if (oldController != null && oldObjects != null) { + for (i = 0, count = _objects.length; i < count; i++) { + oldController.removeObject(oldObjects[i]); + } + } + if (newController != null && newObjects != null) { + for (i = 0, count = newObjects.length; i < count; i++) { + newController.addObject(newObjects[i]); + } + } + } + + /** + * Updates the length of the clip in order to match with length of longest track. + * Should be called after track was changed. + */ + public function updateLength():void { + for (var i:int = 0; i < _numTracks; i++) { + var track:Track = _tracks[i]; + var len:Number = track.length; + if (len > length) { + length = len; + } + } + } + + /** + * Adds a new track to the animation clip. + * The total length of the clip is recalculated automatically. + * + * @param track track which should be added. + * @return added track. + * + * @see #length + */ + public function addTrack(track:Track):Track { + if (track == null) { + throw new Error("Track can not be null"); + } + _tracks[_numTracks++] = track; + if (track.length > length) { + length = track.length; + } + return track; + } + + /** + * Removes the specified track from the clip. The clip length is automatically recalculated. + * + * @param track track which should be removed. + * @return removed track. + * + * @see #length + * @throw Error if the AnimationClip does not include the track. + */ + public function removeTrack(track:Track):Track { + var index:int = _tracks.indexOf(track); + if (index < 0) throw new ArgumentError("Track not found"); + _numTracks--; + var j:int = index + 1; + while (index < _numTracks) { + _tracks[index] = _tracks[j]; + index++; + j++; + } + _tracks.length = _numTracks; + length = 0; + for (var i:int = 0; i < _numTracks; i++) { + var t:Track = _tracks[i]; + if (t.length > length) { + length = t.length; + } + } + return track; + } + + /** + * Returns the track object instance that exists at the specified index. + * + * @param index index. + * @return the track object instance that exists at the specified index. + */ + public function getTrackAt(index:int):Track { + return _tracks[index]; + } + + /** + * Number of tracks in the AnimationClip. + */ + public function get numTracks():int { + return _numTracks; + } + + /** + * @private + */ + override alternativa3d function update(interval:Number, weight:Number):void { + var oldTime:Number = _time; + if (animated) { + _time += interval*speed; + if (loop) { + if (_time < 0) { + // _position = (length <= 0) ? 0 : _position % length; + _time = 0; + } else { + if (_time >= length) { + collectNotifiers(oldTime, length); + _time = (length <= 0) ? 0 : _time % length; + collectNotifiers(0, _time); + } else { + collectNotifiers(oldTime, _time); + } + } + } else { + if (_time < 0) { + _time = 0; + } else if (_time >= length) { + _time = length; + } + collectNotifiers(oldTime, _time); + } + } + if (weight > 0) { + for (var i:int = 0; i < _numTracks; i++) { + var track:Track = _tracks[i]; + if (track.object != null) { + var state:AnimationState = controller.getState(track.object); + if (state != null) { + track.blend(_time, weight, state); + } + } + } + } + } + + /** + * Current time of animation. + */ + public function get time():Number { + return _time; + } + + /** + * @private + */ + public function set time(value:Number):void { + _time = value; + } + + /** + * Current normalized time in the interval [0, 1]. + */ + public function get normalizedTime():Number { + return (length == 0) ? 0 : _time/length; + } + + /** + * @private + */ + public function set normalizedTime(value:Number):void { + _time = value*length; + } + + /** + * @private + */ + private function getNumChildren(object:Object):int { + if (object is Object3D) { + return Object3D(object).numChildren; + } + return 0; + } + + /** + * @private + */ + private function getChildAt(object:Object, index:int):Object { + if (object is Object3D) { + return Object3D(object).getChildAt(index); + } + return null; + } + + /** + * @private + */ + private function addChildren(object:Object):void { + for (var i:int = 0, numChildren:int = getNumChildren(object); i < numChildren; i++) { + var child:Object = getChildAt(object, i); + addObject(child); + addChildren(child); + } + } + + /** + * Binds tracks from the animation clip to given object. Only those tracks which have object property equal to the object's name are bound. + * + * @param object The object to which tracks are bound. + * @param includeDescendants If true, the whole tree of the object's children (if any) is processed. + * + * @see #objects + * @see alternativa.engine3d.animation.keys.Track#object + */ + public function attach(object:Object, includeDescendants:Boolean):void { + updateObjects(_objects, controller, null, controller); + _objects = null; + addObject(object); + if (includeDescendants) { + addChildren(object); + } + } + + /** + * @private + */ + alternativa3d function collectNotifiers(start:Number, end:Number):void { + var notify:AnimationNotify = _notifiersList; + while (notify != null) { + if (notify._time > start) { + if (notify._time > end) { + notify = notify.next; + continue; + } + notify.processNext = controller.nearestNotifyers; + controller.nearestNotifyers = notify; + } + notify = notify.next; + } + } + + /** + * Creates an AnimationNotify instance which is capable of firing notification events when playback reaches the specified time on the time line. + * + * @param time The time in seconds to which the notification trigger will be bound. + * @param name The name of AnimationNotify instance. + * + * @return A new instance of AnimationNotify class bound to specified time counting from start of the time line. + * + * @see AnimationNotify + */ + public function addNotify(time:Number, name:String = null):AnimationNotify { + time = (time <= 0) ? 0 : ((time >= length) ? length : time); + var notify:AnimationNotify = new AnimationNotify(name); + notify._time = time; + if (_notifiersList == null) { + _notifiersList = notify; + return notify; + } else { + if (_notifiersList._time > time) { + // Replaces the first key + notify.next = _notifiersList; + _notifiersList = notify; + return notify; + } else { + // Search for appropriate place + var n:AnimationNotify = _notifiersList; + while (n.next != null && n.next._time <= time) { + n = n.next; + } + if (n.next == null) { + // Places at the end + n.next = notify; + } else { + notify.next = n.next; + n.next = notify; + } + } + } + return notify; + } + + /** + * Creates an AnimationNotify instance which is capable of firing notification events when playback reaches + * the specified time on the time line. The time is specified as an offset from the end of time line towards its start. + * + * @param offsetFromEnd The offset in seconds from the end of the time line towards its start, where the event object will be set in. + * @param name The name of notification trigger. + * + * @return A new instance of AnimationNotify class bound to specified time. + * + * @see AnimationNotify + */ + // TODO: name of method (addNotifyAtEnd) is incomprehensible. Rename to addNotifyFromFinish (or something else). + public function addNotifyAtEnd(offsetFromEnd:Number = 0, name:String = null):AnimationNotify { + return addNotify(length - offsetFromEnd, name); + } + + /** + * Removes specified notification trigger. + * + * @param notify The notification trigger to remove. + * @return The removed notification trigger. + */ + public function removeNotify(notify:AnimationNotify):AnimationNotify { + if (_notifiersList != null) { + if (_notifiersList == notify) { + _notifiersList = _notifiersList.next; + return notify; + } + var n:AnimationNotify = _notifiersList; + while (n.next != null && n.next != notify) { + n = n.next; + } + if (n.next == notify) { + // removes + n.next = notify.next; + return notify; + } + } + throw new Error("Notify not found"); + } + + /** + * The list of notification triggers. + */ + public function get notifiers():Vector. { + var result:Vector. = new Vector.(); + var i:int = 0; + for (var notify:AnimationNotify = _notifiersList; notify != null; notify = notify.next) { + result[i] = notify; + i++; + } + return result; + } + + /** + * Returns a fragment of the clip between specified bounds. + * + * @param start The start time of a fragment in seconds. + * @param end The end time of a fragment in seconds. + * @return The clip fragment. + */ + public function slice(start:Number, end:Number = Number.MAX_VALUE):AnimationClip { + var sliced:AnimationClip = new AnimationClip(name); + sliced._objects = (_objects == null) ? null : [].concat(_objects); + for (var i:int = 0; i < _numTracks; i++) { + sliced.addTrack(_tracks[i].slice(start, end)); + } + return sliced; + } + + /** + * Clones the clip. Both the clone and the original reference the same tracks. + */ + public function clone():AnimationClip { + var cloned:AnimationClip = new AnimationClip(name); + cloned._objects = (_objects == null) ? null : [].concat(_objects); + for (var i:int = 0; i < _numTracks; i++) { + cloned.addTrack(_tracks[i]); + } + cloned.length = length; + return cloned; + } + + } +} diff --git a/src/alternativa/engine3d/animation/AnimationController.as b/src/alternativa/engine3d/animation/AnimationController.as new file mode 100644 index 0000000..7642281 --- /dev/null +++ b/src/alternativa/engine3d/animation/AnimationController.as @@ -0,0 +1,220 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.animation.events.NotifyEvent; + import alternativa.engine3d.core.Object3D; + + import flash.utils.Dictionary; + import flash.utils.getTimer; + + use namespace alternativa3d; + /** + * Controls animation playback and blending. I.e. it animates model using information + * stored in AnimationClip-s and generated by AnimationSwitcher + * and AnimationCouple blenders. + * You have to call method update() each frame, + * which refreshes all child animation clips and blenders, which return + * list of properties and values to controller after that. You can use this list + * to set those properties. Controller sets those values and as a result + * the animation goes on. Animation control is carried out with the + * help of animated flag, and with AnimationSwitcher blender, + * which can transfer clip from active state to passive and vice versa. + * + * + * @see alternativa.engine3d.animation.AnimationClip + * @see alternativa.engine3d.animation.AnimationCouple + * @see alternativa.engine3d.animation.AnimationSwitcher + */ + public class AnimationController { + + /** + * @private + */ + private var _root:AnimationNode; + + /** + * @private + */ + private var _objects:Vector.; + /** + * @private + */ + private var _object3ds:Vector. = new Vector.(); + /** + * @private + */ + private var objectsUsedCount:Dictionary = new Dictionary(); + + /** + * @private + */ + private var states:Object = new Object(); +// private var datasList:BlendedData; + + /** + * @private + */ + private var lastTime:int = -1; + + /** + * @private + */ + alternativa3d var nearestNotifyers:AnimationNotify; + + /** + * Creates a AnimationController object. + */ + public function AnimationController() { + } + + /** + * Root of the animation tree. + */ + public function get root():AnimationNode { + return _root; + } + + /** + * @private + */ + public function set root(value:AnimationNode):void { + if (_root != value) { + if (_root != null) { + _root.setController(null); + _root._isActive = false; + } + if (value != null) { + value.setController(this); + value._isActive = true; + } + this._root = value; + } + } + + /** + * Plays animations on the time interval passed since the last update() call. + * If freeze() method was called after the last update(), + * animation will continue from that moment. + */ + public function update():void { + var interval:Number; + if (lastTime < 0) { + lastTime = getTimer(); + interval = 0; + } else { + var time:int = getTimer(); + interval = 0.001*(time - lastTime); + lastTime = time; + } + if (_root == null) { + return; + } + var data:AnimationState; + // Cleaning + for each (data in states) { + data.reset(); + } + _root.update(interval, 1); + // Apply the animation + for (var i:int = 0, count:int = _object3ds.length; i < count; i++) { + var object:Object3D = _object3ds[i]; + data = states[object.name]; + if (data != null) { + data.apply(object); + } + } + // Calls the notifications + for (var notify:AnimationNotify = nearestNotifyers; notify != null; notify = notify.processNext) { + if (notify.willTrigger(NotifyEvent.NOTIFY)) { + notify.dispatchEvent(new NotifyEvent(notify)); + } + } + nearestNotifyers = null; + } + + /** + * @private + */ + alternativa3d function addObject(object:Object):void { + if (object in objectsUsedCount) { + objectsUsedCount[object]++; + } else { + if (object is Object3D) { + _object3ds.push(object); + } else { + _objects.push(object); + } + objectsUsedCount[object] = 1; + } + } + + /** + * @private + */ + alternativa3d function removeObject(object:Object):void { + var used:int = objectsUsedCount[object]; + used--; + if (used <= 0) { + var index:int; + var j:int; + var count:int; + if (object is Object3D) { + index = _object3ds.indexOf(object); + count = _object3ds.length - 1; + j = index + 1; + while (index < count) { + _object3ds[index] = _object3ds[j]; + index++; + j++; + } + _object3ds.length = count; + } else { + index = _objects.indexOf(object); + count = _objects.length - 1; + j = index + 1; + while (index < count) { + _objects[index] = _objects[j]; + index++; + j++; + } + _objects.length = count; + } + delete objectsUsedCount[object]; + } else { + objectsUsedCount[object] = used; + } + } + + /** + * @private + */ + alternativa3d function getState(name:String):AnimationState { + var state:AnimationState = states[name]; + if (state == null) { + state = new AnimationState(); + states[name] = state; + } + return state; + } + + /** + * Freezes internal time counter till the next update() call. + * + * @see #update + */ + public function freeze():void { + lastTime = -1; + } + + } +} diff --git a/src/alternativa/engine3d/animation/AnimationCouple.as b/src/alternativa/engine3d/animation/AnimationCouple.as new file mode 100644 index 0000000..5ab0232 --- /dev/null +++ b/src/alternativa/engine3d/animation/AnimationCouple.as @@ -0,0 +1,138 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation { + + import alternativa.engine3d.alternativa3d; + + use namespace alternativa3d; + + /** + * Blends two animations according to the balance value. + * Mixes two animations with the given percentage. Any of AnimationClip, + * AnimationSwitcher, AnimationCouple classes can be blended. + */ + public class AnimationCouple extends AnimationNode { + + /** + * @private + */ + private var _left:AnimationNode; + /** + * @private + */ + private var _right:AnimationNode; + + /** + * The balance is a value in [0, 1] interval which specifies weight coefficient for each animation. + * The first (left) animation gets weight of (1 - balance) and the second (right) one gets weigth of balance. + */ + public var balance:Number = 0.5; + + /** + * @private + */ + override alternativa3d function update(elapsed:Number, weight:Number):void { + var w:Number = (balance <= 0) ? 0 : ((balance >= 1) ? 1 : balance); + if (_left == null) { + _right.update(elapsed*speed, weight); + } else if (_right == null) { + _left.update(elapsed*speed, weight); + } else { + _left.update(elapsed*speed, (1 - w)*weight); + _right.update(elapsed*speed, w*weight); + } + } + + /** + * @private + */ + override alternativa3d function setController(value:AnimationController):void { + this.controller = value; + if (_left != null) { + _left.setController(value); + } + if (_right != null) { + _right.setController(value); + } + } + + /** + * @private + */ + override alternativa3d function addNode(node:AnimationNode):void { + super.addNode(node); + node._isActive = true; + } + + /** + * @private + */ + override alternativa3d function removeNode(node:AnimationNode):void { + if (_left == node) { + _left = null; + } else { + _right = null; + } + super.removeNode(node); + } + + /** + * The first animation. + */ + public function get left():AnimationNode { + return _left; + } + + /** + * @private + */ + public function set left(value:AnimationNode):void { + if (value != _left) { + if (value._parent == this) { + throw new Error("Animation already exists in blender"); + } + if (_left != null) { + removeNode(_left); + } + _left = value; + if (value != null) { + addNode(value); + } + } + } + + /** + * The second animation. + */ + public function get right():AnimationNode { + return _right; + } + + /** + * @private + */ + public function set right(value:AnimationNode):void { + if (value != _right) { + if (value._parent == this) { + throw new Error("Animation already exists in blender"); + } + if (_right != null) { + removeNode(_right); + } + _right = value; + if (value != null) { + addNode(value); + } + } + } + + } +} diff --git a/src/alternativa/engine3d/animation/AnimationNode.as b/src/alternativa/engine3d/animation/AnimationNode.as new file mode 100644 index 0000000..6b6e5c0 --- /dev/null +++ b/src/alternativa/engine3d/animation/AnimationNode.as @@ -0,0 +1,96 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation { + + import alternativa.engine3d.alternativa3d; + + use namespace alternativa3d; + + /** + * Animation tree node. Animation in Alternativa3D is built over the blend tree. + * This tree is intended for combining a set of animations and keeping unambiguous status + * of each property being animated in every frame. E.g. there can be independent animations + * for legs and hands, that will be presented by the nodes of blend tree. With the help of blenders, + * derived from AnimationNode you can change or blend nodes of blend tree. Every tree animation + * is controlled by AnimationController. AnimationNode instance have to be a root element of the tree. + * + */ + public class AnimationNode { + + /** + * @private + */ + alternativa3d var _isActive:Boolean = false; + + /** + * @private + */ + alternativa3d var _parent:AnimationNode; + /** + * @private + */ + alternativa3d var controller:AnimationController; + + /** + * Animation speed. + */ + public var speed:Number = 1; + + /** + * Determines if the animation is active. + */ + public function get isActive():Boolean { + return _isActive && controller != null; + } + + /** + * Parent of this node in animation tree hierarchy. + */ + public function get parent():AnimationNode { + return _parent; + } + + + /** + * @private + */ + alternativa3d function update(elapsed:Number, weight:Number):void { + } + + /** + * @private + */ + alternativa3d function setController(value:AnimationController):void { + this.controller = value; + } + + /** + * @private + */ + alternativa3d function addNode(node:AnimationNode):void { + if (node._parent != null) { + node._parent.removeNode(node); + } + node._parent = this; + node.setController(controller); + } + + /** + * @private + */ + alternativa3d function removeNode(node:AnimationNode):void { + node.setController(null); + node._isActive = false; + node._parent = null; + } + + } +} diff --git a/src/alternativa/engine3d/animation/AnimationNotify.as b/src/alternativa/engine3d/animation/AnimationNotify.as new file mode 100644 index 0000000..505fa53 --- /dev/null +++ b/src/alternativa/engine3d/animation/AnimationNotify.as @@ -0,0 +1,78 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation { + + import alternativa.engine3d.alternativa3d; + + import flash.events.EventDispatcher; + + use namespace alternativa3d; + + /** + * The notification trigger bound to certain time on an animation time line. + * AnimationNotify instance subscribes to NotifyEvent.When animation + * playback reaches the given time, an event is dispatched by the trigger. + * + * + * animationClip.addNotify(30).addEventListener(NotifyEvent.NOTIFY, notifyHandler) + * … + * private function notifyHandler(e:NotifyEvent):void{ + * trace("Animation time is " + e.notify.time + " seconds now") + *} + * + * + * @see AnimationClip#addNotify() + * @see AnimationClip#addNotifyAtEnd() + */ + public class AnimationNotify extends EventDispatcher { + + /** + * The name of notification trigger. + */ + public var name:String; + + /** + * @private + */ + alternativa3d var _time:Number = 0; + /** + * @private + */ + alternativa3d var next:AnimationNotify; + + /** + * @private + */ + alternativa3d var updateTime:Number; + /** + * @private + */ + alternativa3d var processNext:AnimationNotify; + + /** + * A new instance should not be created directly. Instead, use AnimationClip.addNotify() or AnimationClip.addNotifyAtEnd() methods. + * + * @see AnimationClip#addNotify() + * @see AnimationClip#addNotifyAtEnd() + */ + public function AnimationNotify(name:String) { + this.name = name; + } + + /** + * The time in seconds on the time line to which the trigger is bound. + */ + public function get time():Number { + return _time; + } + + } +} diff --git a/src/alternativa/engine3d/animation/AnimationState.as b/src/alternativa/engine3d/animation/AnimationState.as new file mode 100644 index 0000000..1694b28 --- /dev/null +++ b/src/alternativa/engine3d/animation/AnimationState.as @@ -0,0 +1,262 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.animation.keys.TransformKey; + import alternativa.engine3d.core.Object3D; + + import flash.geom.Vector3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class AnimationState { + + public var useCount:int = 0; + + public var transform:TransformKey = new TransformKey(); + public var transformWeightSum:Number = 0; + + public var numbers:Object = new Object(); + public var numberWeightSums:Object = new Object(); + + + public function AnimationState() { + } + + public function reset():void { + transformWeightSum = 0; + for (var key:String in numbers) { + delete numbers[key]; + delete numberWeightSums[key]; + } + } + + public function addWeightedTransform(key:TransformKey, weight:Number):void { + transformWeightSum += weight; + transform.interpolate(transform, key, weight/transformWeightSum); + } + + public function addWeightedNumber(property:String, value:Number, weight:Number):void { + var sum:Number = numberWeightSums[property]; + if (sum == sum) { + sum += weight; + weight /= sum; + var current:Number = numbers[property]; + numbers[property] = (1 - weight)*current + weight*value; + numberWeightSums[property] = sum; + } else { + numbers[property] = value; + numberWeightSums[property] = weight; + } + } + + public function apply(object:Object3D):void { + if (transformWeightSum > 0) { + object._x = transform.x; + object._y = transform.y; + object._z = transform.z; + setEulerAngles(transform.rotation, object); + object._scaleX = transform.scaleX; + object._scaleY = transform.scaleY; + object._scaleZ = transform.scaleZ; + object.transformChanged = true; + } + + var sum:Number, weight:Number; + for (var key:String in numbers) { + switch (key) { + case 'x': + sum = numberWeightSums['x']; + weight = sum/(sum + transformWeightSum); + object.x = (1 - weight)*object.x + weight*numbers['x']; + break; + case 'y': + sum = numberWeightSums['y']; + weight = sum/(sum + transformWeightSum); + object.y = (1 - weight)*object.y + weight*numbers['y']; + break; + case 'z': + sum = numberWeightSums['z']; + weight = sum/(sum + transformWeightSum); + object.z = (1 - weight)*object.z + weight*numbers['z']; + break; + case 'rotationX': + sum = numberWeightSums['rotationX']; + weight = sum/(sum + transformWeightSum); + object.rotationX = (1 - weight)*object.rotationX + weight*numbers['rotationX']; + break; + case 'rotationY': + sum = numberWeightSums['rotationY']; + weight = sum/(sum + transformWeightSum); + object.rotationY = (1 - weight)*object.rotationY + weight*numbers['rotationY']; + break; + case 'rotationZ': + sum = numberWeightSums['rotationZ']; + weight = sum/(sum + transformWeightSum); + object.rotationZ = (1 - weight)*object.rotationZ + weight*numbers['rotationZ']; + break; + case 'scaleX': + sum = numberWeightSums['scaleX']; + weight = sum/(sum + transformWeightSum); + object.scaleX = (1 - weight)*object.scaleX + weight*numbers['scaleX']; + break; + case 'scaleY': + sum = numberWeightSums['scaleY']; + weight = sum/(sum + transformWeightSum); + object.scaleY = (1 - weight)*object.scaleY + weight*numbers['scaleY']; + break; + case 'scaleZ': + sum = numberWeightSums['scaleZ']; + weight = sum/(sum + transformWeightSum); + object.scaleZ = (1 - weight)*object.scaleZ + weight*numbers['scaleZ']; + break; + default : + object[key] = numbers[key]; + break; + } + } + } + + public function applyObject(object:Object):void { + if (transformWeightSum > 0) { + object.x = transform.x; + object.y = transform.y; + object.z = transform.z; + setEulerAnglesObject(transform.rotation, object); + object.scaleX = transform.scaleX; + object.scaleY = transform.scaleY; + object.scaleZ = transform.scaleZ; + } + + var sum:Number, weight:Number; + for (var key:String in numbers) { + switch (key) { + case 'x': + sum = numberWeightSums['x']; + weight = sum/(sum + transformWeightSum); + object.x = (1 - weight)*object.x + weight*numbers['x']; + break; + case 'y': + sum = numberWeightSums['y']; + weight = sum/(sum + transformWeightSum); + object.y = (1 - weight)*object.y + weight*numbers['y']; + break; + case 'z': + sum = numberWeightSums['z']; + weight = sum/(sum + transformWeightSum); + object.z = (1 - weight)*object.z + weight*numbers['z']; + break; + case 'rotationX': + sum = numberWeightSums['rotationX']; + weight = sum/(sum + transformWeightSum); + object.rotationX = (1 - weight)*object.rotationX + weight*numbers['rotationX']; + break; + case 'rotationY': + sum = numberWeightSums['rotationY']; + weight = sum/(sum + transformWeightSum); + object.rotationY = (1 - weight)*object.rotationY + weight*numbers['rotationY']; + break; + case 'rotationZ': + sum = numberWeightSums['rotationZ']; + weight = sum/(sum + transformWeightSum); + object.rotationZ = (1 - weight)*object.rotationZ + weight*numbers['rotationZ']; + break; + case 'scaleX': + sum = numberWeightSums['scaleX']; + weight = sum/(sum + transformWeightSum); + object.scaleX = (1 - weight)*object.scaleX + weight*numbers['scaleX']; + break; + case 'scaleY': + sum = numberWeightSums['scaleY']; + weight = sum/(sum + transformWeightSum); + object.scaleY = (1 - weight)*object.scaleY + weight*numbers['scaleY']; + break; + case 'scaleZ': + sum = numberWeightSums['scaleZ']; + weight = sum/(sum + transformWeightSum); + object.scaleZ = (1 - weight)*object.scaleZ + weight*numbers['scaleZ']; + break; + default : + object[key] = numbers[key]; + break; + } + } + } + + private function setEulerAngles(quat:Vector3D, object:Object3D):void { + var qi2:Number = 2*quat.x*quat.x; + var qj2:Number = 2*quat.y*quat.y; + var qk2:Number = 2*quat.z*quat.z; + var qij:Number = 2*quat.x*quat.y; + var qjk:Number = 2*quat.y*quat.z; + var qki:Number = 2*quat.z*quat.x; + var qri:Number = 2*quat.w*quat.x; + var qrj:Number = 2*quat.w*quat.y; + var qrk:Number = 2*quat.w*quat.z; + + var aa:Number = 1 - qj2 - qk2; + var bb:Number = qij - qrk; + var ee:Number = qij + qrk; + var ff:Number = 1 - qi2 - qk2; + var ii:Number = qki - qrj; + var jj:Number = qjk + qri; + var kk:Number = 1 - qi2 - qj2; + + if (-1 < ii && ii < 1) { + object._rotationX = Math.atan2(jj, kk); + object._rotationY = -Math.asin(ii); + object._rotationZ = Math.atan2(ee, aa); + } else { + object._rotationX = 0; + object._rotationY = (ii <= -1) ? Math.PI : -Math.PI; + object._rotationY *= 0.5; + object._rotationZ = Math.atan2(-bb, ff); + } + } + + private function setEulerAnglesObject(quat:Vector3D, object:Object):void { + var qi2:Number = 2*quat.x*quat.x; + var qj2:Number = 2*quat.y*quat.y; + var qk2:Number = 2*quat.z*quat.z; + var qij:Number = 2*quat.x*quat.y; + var qjk:Number = 2*quat.y*quat.z; + var qki:Number = 2*quat.z*quat.x; + var qri:Number = 2*quat.w*quat.x; + var qrj:Number = 2*quat.w*quat.y; + var qrk:Number = 2*quat.w*quat.z; + + var aa:Number = 1 - qj2 - qk2; + var bb:Number = qij - qrk; + var ee:Number = qij + qrk; + var ff:Number = 1 - qi2 - qk2; + var ii:Number = qki - qrj; + var jj:Number = qjk + qri; + var kk:Number = 1 - qi2 - qj2; + + if (-1 < ii && ii < 1) { + object.rotationX = Math.atan2(jj, kk); + object.rotationY = -Math.asin(ii); + object.rotationZ = Math.atan2(ee, aa); + } else { + object.rotationX = 0; + object.rotationY = (ii <= -1) ? Math.PI : -Math.PI; + object.rotationY *= 0.5; + object.rotationZ = Math.atan2(-bb, ff); + } + } + + + } +} diff --git a/src/alternativa/engine3d/animation/AnimationSwitcher.as b/src/alternativa/engine3d/animation/AnimationSwitcher.as new file mode 100644 index 0000000..5291749 --- /dev/null +++ b/src/alternativa/engine3d/animation/AnimationSwitcher.as @@ -0,0 +1,198 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation { + + import alternativa.engine3d.alternativa3d; + + use namespace alternativa3d; + + /** + * The animation switcher performs animation blending and active animation switching. + * + */ + public class AnimationSwitcher extends AnimationNode { + + /** + * @private + */ + private var _numAnimations:int = 0; + /** + * @private + */ + private var _animations:Vector. = new Vector.(); + /** + * @private + */ + private var _weights:Vector. = new Vector.(); + /** + * @private + */ + private var _active:AnimationNode; + /** + * @private + */ + private var fadingSpeed:Number = 0; + + /** + * @private + */ + override alternativa3d function update(elapsed:Number, weight:Number):void { + // TODO : make fade if it required only + var interval:Number = speed * elapsed; + var fade:Number = fadingSpeed * interval; + for (var i:int = 0; i < _numAnimations; i++) { + var animation:AnimationNode = _animations[i]; + var w:Number = _weights[i]; + if (animation == _active) { + w += fade; + w = (w >= 1) ? 1 : w; + animation.update(interval, weight * w); + _weights[i] = w; + } else { + w -= fade; + if (w > 0) { + animation.update(interval, weight * w); + _weights[i] = w; + } else { + animation._isActive = false; + _weights[i] = 0; + } + } + } + } + + /** + * The current active animation. To change active animation use activate(). + * + * @see #activate() + */ + public function get active():AnimationNode { + return _active; + } + + /** + * Activates specified animation during given time interval. All the rest animations fade out. + * + * @param animation Animation which is set as active. + * @param time The time interval during which the animation becomes fully active (i.e. has full weight). + */ + public function activate(animation:AnimationNode, time:Number = 0):void { + if (animation._parent != this) { + throw new Error("Animation is not child of this blender"); + } + _active = animation; + animation._isActive = true; + if (time <= 0) { + for (var i:int = 0; i < _numAnimations; i++) { + if (_animations[i] == animation) { + _weights[i] = 1; + } else { + _weights[i] = 0; + _animations[i]._isActive = false; + } + } + fadingSpeed = 0; + } else { + fadingSpeed = 1/time; + } + } + + /** + * @private + */ + override alternativa3d function setController(value:AnimationController):void { + this.controller = value; + for (var i:int = 0; i < _numAnimations; i++) { + var animation:AnimationNode = _animations[i]; + animation.setController(controller); + } + } + + /** + * @private + */ + override alternativa3d function removeNode(node:AnimationNode):void { + removeAnimation(node); + } + + /** + * Adds a new animation. + * + * @param animation The animation node to add. + * @return Added animation. + */ + public function addAnimation(animation:AnimationNode):AnimationNode { + if (animation == null) { + throw new Error("Animation cannot be null"); + } + if (animation._parent == this) { + throw new Error("Animation already exist in blender"); + } + _animations[_numAnimations] = animation; + if (_numAnimations == 0) { + _active = animation; + animation._isActive = true; + _weights[_numAnimations] = 1; + } else { + _weights[_numAnimations] = 0; + } + _numAnimations++; + addNode(animation); + return animation; + } + + /** + * Removes child animation node. + * + * @param animation Animation node to remove. + */ + public function removeAnimation(animation:AnimationNode):AnimationNode { + var index:int = _animations.indexOf(animation); + if (index < 0) throw new ArgumentError("Animation not found"); + _numAnimations--; + var j:int = index + 1; + while (index < _numAnimations) { + _animations[index] = _animations[j]; + index++; + j++; + } + _animations.length = _numAnimations; + _weights.length = _numAnimations; + if (_active == animation) { + if (_numAnimations > 0) { + _active = _animations[int(_numAnimations - 1)]; + _weights[int(_numAnimations - 1)] = 1; + } else { + _active = null; + } + } + super.removeNode(animation); + return animation; + } + + /** + * Returns the child animation that exists at the specified index. + * + * @param index The index position of the child object. + */ + public function getAnimationAt(index:int):AnimationNode { + return _animations[index]; + } + + /** + * Returns number of animations. + */ + public function numAnimations():int { + return _numAnimations; + } + + } +} diff --git a/src/alternativa/engine3d/animation/events/NotifyEvent.as b/src/alternativa/engine3d/animation/events/NotifyEvent.as new file mode 100644 index 0000000..cb5c44c --- /dev/null +++ b/src/alternativa/engine3d/animation/events/NotifyEvent.as @@ -0,0 +1,44 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation.events { + + import alternativa.engine3d.animation.AnimationNotify; + + import flash.events.Event; + + /** + * This event is fired by an AnimationNotify instance when certain point of AnimationClip time line is reached. + * + * @see alternativa.engine3d.animation.AnimationNotify + */ + public class NotifyEvent extends Event { + + /** + *NotifyEvent.NOTIFY is specified as the type property for transfer to the addEventListener alert for the event. + */ + public static const NOTIFY:String = "notify"; + + /** + * The source of the event. Actually this property returns AnimationNotify(target) value. + */ + public function get notify():AnimationNotify { + return AnimationNotify(target); + } + + /** + * Creates a Notyfy object. + */ + public function NotifyEvent(notify:AnimationNotify) { + super(NOTIFY); + } + + } +} diff --git a/src/alternativa/engine3d/animation/keys/Keyframe.as b/src/alternativa/engine3d/animation/keys/Keyframe.as new file mode 100644 index 0000000..2aecba0 --- /dev/null +++ b/src/alternativa/engine3d/animation/keys/Keyframe.as @@ -0,0 +1,84 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation.keys { + + import alternativa.engine3d.alternativa3d; + + use namespace alternativa3d; + + /** + * Keyframe of the animation. Sets object property at given time. + * Keyframe animation can be defined with NumberTrack and TransformTrack classes. + * + * @see TransformTrack + * @see NumberTrack + */ + public class Keyframe { + + /** + * @private + * Key frame time in seconds. + */ + alternativa3d var _time:Number = 0; + + /** + * Creates a new Keyframe instance. + */ + public function Keyframe() { + } + + /** + * Key frame time in seconds. + */ + public function get time():Number { + return _time; + } + + /** + * The value of animated property kept by the keyframe. + * Can be Number or Matrix3D depends on + * NumberTrack or TransformTrack belongs to. + * + * @see NumberTrack + * @see TransformTrack + */ + public function get value():Object { + return null; + } + + /** + * @private + */ + public function set value(v:Object):void { + } + + /** + * @private + */ + alternativa3d function get nextKeyFrame():Keyframe { + return null; + } + + /** + * @private + */ + alternativa3d function set nextKeyFrame(value:Keyframe):void { + } + + /** + * Returns string representation of the object. + */ + public function toString():String { + return '[Keyframe time = ' + _time.toFixed(2) + ' value = ' + value + ']'; + } + + } +} diff --git a/src/alternativa/engine3d/animation/keys/NumberKey.as b/src/alternativa/engine3d/animation/keys/NumberKey.as new file mode 100644 index 0000000..2ecf935 --- /dev/null +++ b/src/alternativa/engine3d/animation/keys/NumberKey.as @@ -0,0 +1,73 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation.keys { + + import alternativa.engine3d.alternativa3d; + + use namespace alternativa3d; + + /** + * @private + */ + public class NumberKey extends Keyframe { + + /** + * @private + */ + alternativa3d var _value:Number = 0; + /** + * @private + */ + alternativa3d var next:NumberKey; + + /** + * Creates a NumberKey object. + */ + public function NumberKey() { + } + + /** + * Sets interpolated value. + */ + public function interpolate(a:NumberKey, b:NumberKey, c:Number):void { + _value = (1 - c)*a._value + c*b._value; + } + + /** + * @inheritDoc + */ + override public function get value():Object { + return _value; + } + + /** + * @inheritDoc + */ + override public function set value(v:Object):void { + _value = Number(v); + } + + /** + * @inheritDoc + */ + override alternativa3d function get nextKeyFrame():Keyframe { + return next; + } + + /** + * @inheritDoc + */ + override alternativa3d function set nextKeyFrame(value:Keyframe):void { + next = NumberKey(value); + } + + } +} diff --git a/src/alternativa/engine3d/animation/keys/NumberTrack.as b/src/alternativa/engine3d/animation/keys/NumberTrack.as new file mode 100644 index 0000000..949aebe --- /dev/null +++ b/src/alternativa/engine3d/animation/keys/NumberTrack.as @@ -0,0 +1,160 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation.keys { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.animation.AnimationState; + + use namespace alternativa3d; + + /** + * + * Keyframe track for animating numeric properties. Each keyframe keeps its own value of the property. + * The value interpolates for in between keyframes. + */ + public class NumberTrack extends Track { + + /** + * @private + * Head of keyframe list. + */ + alternativa3d var keyList:NumberKey; + + private var lastKey:NumberKey; + + /** + * @private + */ + override alternativa3d function get keyFramesList():Keyframe { + return keyList; + } + + /** + * @private + */ + override alternativa3d function set keyFramesList(value:Keyframe):void { + keyList = NumberKey(value); + } + + + /** + * @private + */ + override alternativa3d function get lastKey():Keyframe { + return lastKey; + } + + + /** + * @private + */ + override alternativa3d function set lastKey(value:Keyframe):void { + lastKey = NumberKey(value); + } + + /** + * Defines the name of object property which will be animated. + */ + public var property:String; + + /** + * Creates a NumberTrack object. + * + * @param object name of animating object. + * @param property name of animating property. + */ + public function NumberTrack(object:String, property:String) { + this.property = property; + this.object = object; + } + + /** + * Adds new keyframe. Keyframes stores ordered by its time property. + * + * @param time time of the new keyframe. + * @param value value of property for the new keyframe. + * @return added keyframe. + */ + public function addKey(time:Number, value:Number = 0):Keyframe { + var key:NumberKey = new NumberKey(); + key._time = time; + key.value = value; + addKeyToList(key); + return key; + } + + /** + * @private + */ + private static var temp:NumberKey = new NumberKey(); + + private var recentKey:NumberKey = null; + + /** + * @private + */ + override alternativa3d function blend(time:Number, weight:Number, state:AnimationState):void { + if (property == null) { + return; + } + var prev:NumberKey; + var next:NumberKey; + + if (recentKey != null && recentKey.time < time) { + prev = recentKey; + next = recentKey.next; + } else { + next = keyList; + } + while (next != null && next._time < time) { + prev = next; + next = next.next; + } + if (prev != null) { + if (next != null) { + temp.interpolate(prev, next, (time - prev._time)/(next._time - prev._time)); + state.addWeightedNumber(property, temp._value, weight); + } else { + state.addWeightedNumber(property, prev._value, weight); + } + recentKey = prev; + } else { + if (next != null) { + state.addWeightedNumber(property, next._value, weight); + } + } + } + + /** + * @private + */ + override alternativa3d function createKeyFrame():Keyframe { + return new NumberKey(); + } + + /** + * @private + */ + override alternativa3d function interpolateKeyFrame(dest:Keyframe, a:Keyframe, b:Keyframe, value:Number):void { + NumberKey(dest).interpolate(NumberKey(a), NumberKey(b), value); + } + + /** + * @inheritDoc + */ + override public function slice(start:Number, end:Number = Number.MAX_VALUE):Track { + var track:NumberTrack = new NumberTrack(object, property); + sliceImplementation(track, start, end); + return track; + } + + } +} diff --git a/src/alternativa/engine3d/animation/keys/Track.as b/src/alternativa/engine3d/animation/keys/Track.as new file mode 100644 index 0000000..4226111 --- /dev/null +++ b/src/alternativa/engine3d/animation/keys/Track.as @@ -0,0 +1,261 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation.keys { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.animation.AnimationState; + + use namespace alternativa3d; + + /** + * Keyframe track baseclass. + * + * @see alternativa.engine3d.animation.AnimationClip + */ + public class Track { + + /** + * Name of the object which is animated. + */ + public var object:String; + + /** + * @private + */ + alternativa3d var _length:Number = 0; + + /** + * Creates a Track object. + */ + public function Track() { + } + + /** + * The length of animation in seconds.. + */ + public function get length():Number { + return _length; + } + + /** + * @private + */ + alternativa3d function get keyFramesList():Keyframe { + return null; + } + + /** + * @private + */ + alternativa3d function set keyFramesList(value:Keyframe):void { + } + + /** + * @private + */ + alternativa3d function get lastKey():Keyframe { + return null; + } + + /** + * @private + */ + alternativa3d function set lastKey(value:Keyframe):void { + + } + + /** + * @private + */ + alternativa3d function addKeyToList(key:Keyframe):void { + var time:Number = key._time; + if (keyFramesList == null) { + keyFramesList = key; + lastKey = key; + _length = (time <= 0) ? 0 : time; + return; + } else { + if (keyFramesList._time > time) { + // replace head of the keyframe list + key.nextKeyFrame = keyFramesList; + keyFramesList = key; + return; + } else { + // adds to the end of list + if (lastKey._time < time) { + lastKey.nextKeyFrame = key; + lastKey = key; + _length = (time <= 0) ? 0 : time; + } else { + // search for appropriate place + var k:Keyframe = keyFramesList; + while (k.nextKeyFrame != null && k.nextKeyFrame._time <= time) { + k = k.nextKeyFrame; + } + if (k.nextKeyFrame == null) { + // adds to the end + k.nextKeyFrame = key; + _length = (time <= 0) ? 0 : time; + } else { + key.nextKeyFrame = k.nextKeyFrame; + k.nextKeyFrame = key; + } + } + + } + } + } + + /** + * Removes the supplied key frame. + * + * @param key the key frame to remove. + * @return removed key frame. + */ + public function removeKey(key:Keyframe):Keyframe { + if (keyFramesList != null) { + if (keyFramesList == key) { + keyFramesList = keyFramesList.nextKeyFrame; + if (keyFramesList == null) { + lastKey = null; + _length = 0; + } + return key; + } + var k:Keyframe = keyFramesList; + while (k.nextKeyFrame != null && k.nextKeyFrame != key) { + k = k.nextKeyFrame; + } + if (k.nextKeyFrame == key) { + // Remove + if (key.nextKeyFrame == null) { + lastKey = k; + // Last item + _length = (k._time <= 0) ? 0 : k._time; + } + k.nextKeyFrame = key.nextKeyFrame; + return key; + } + } + throw new Error("Key not found"); + } + + /** + * Time-sorted list of key frames. + */ + public function get keys():Vector. { + var result:Vector. = new Vector.(); + var i:int = 0; + for (var key:Keyframe = keyFramesList; key != null; key = key.nextKeyFrame) { + result[i] = key; + i++; + } + return result; + } + + /** + * @private + */ + alternativa3d function blend(time:Number, weight:Number, state:AnimationState):void { + } + + /** + * Returns a fragment of animation track between start and end time. + * + * @param start Fragment's start time. + * @param end Fragment's end time. + * @return Track fragment. + */ + public function slice(start:Number, end:Number = Number.MAX_VALUE):Track { + return null; + } + + /** + * @private + */ + alternativa3d function createKeyFrame():Keyframe { + return null; + } + + /** + * @private + */ + alternativa3d function interpolateKeyFrame(dest:Keyframe, a:Keyframe, b:Keyframe, value:Number):void { + } + + /** + * @private + */ + alternativa3d function sliceImplementation(dest:Track, start:Number, end:Number):void { + var shiftTime:Number = (start > 0) ? start : 0; + var prev:Keyframe; + var next:Keyframe = keyFramesList; + // the first keyframe + var key:Keyframe = createKeyFrame(); + var nextKey:Keyframe; + while (next != null && next._time <= start) { + prev = next; + next = next.nextKeyFrame; + } + if (prev != null) { + if (next != null) { + interpolateKeyFrame(key, prev, next, (start - prev._time)/(next._time - prev._time)); + key._time = start - shiftTime; + } else { + // last keyframe + interpolateKeyFrame(key, key, prev, 1); + } + } else { + if (next != null) { + // time before the start of animation + interpolateKeyFrame(key, key, next, 1); + key._time = next._time - shiftTime; + prev = next; + next = next.nextKeyFrame; + } else { + // empty track + return; + } + } + dest.keyFramesList = key; + if (next == null || end <= start) { + // one key frame + dest._length = (key._time <= 0) ? 0 : key._time; + return; + } + // copies intermediate keys + while (next != null && next._time <= end) { + nextKey = createKeyFrame(); + interpolateKeyFrame(nextKey, nextKey, next, 1); + nextKey._time = next._time - shiftTime; + key.nextKeyFrame = nextKey; + key = nextKey; + prev = next; + next = next.nextKeyFrame; + } + // move last key + if (next != null) { + // time to end of the track + nextKey = createKeyFrame(); + interpolateKeyFrame(nextKey, prev, next, (end - prev._time)/(next._time - prev._time)); + nextKey._time = end - shiftTime; + key.nextKeyFrame = nextKey; + } else { + // time after current key + } + if (nextKey != null) { + dest._length = (nextKey._time <= 0) ? 0 : nextKey._time; + } + return; + } + + } +} diff --git a/src/alternativa/engine3d/animation/keys/TransformKey.as b/src/alternativa/engine3d/animation/keys/TransformKey.as new file mode 100644 index 0000000..f6aed07 --- /dev/null +++ b/src/alternativa/engine3d/animation/keys/TransformKey.as @@ -0,0 +1,164 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation.keys { + + import alternativa.engine3d.alternativa3d; + + import flash.geom.Matrix3D; + import flash.geom.Orientation3D; + import flash.geom.Vector3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class TransformKey extends Keyframe { + + /** + * @private + */ + alternativa3d var x:Number = 0; + /** + * @private + */ + alternativa3d var y:Number = 0; + /** + * @private + */ + alternativa3d var z:Number = 0; + /** + * @private + */ + alternativa3d var rotation:Vector3D = new Vector3D(0, 0, 0, 1); + /** + * @private + */ + alternativa3d var scaleX:Number = 1; + /** + * @private + */ + alternativa3d var scaleY:Number = 1; + /** + * @private + */ + alternativa3d var scaleZ:Number = 1; + + /** + * @private + */ + alternativa3d var next:TransformKey; + + /** + * Creates a TransformKey object. + */ + public function TransformKey() { + } + + /** + * @inheritDoc + */ + override public function get value():Object { + var m:Matrix3D = new Matrix3D(); + m.recompose(Vector.([new Vector3D(x, y, z), rotation, new Vector3D(scaleX, scaleY, scaleZ)]), Orientation3D.QUATERNION); + return m; + } + + /** + * @inheritDoc + */ + override public function set value(v:Object):void { + var m:Matrix3D = Matrix3D(v); + var components:Vector. = m.decompose(Orientation3D.QUATERNION); + x = components[0].x; + y = components[0].y; + z = components[0].z; + rotation = components[1]; + scaleX = components[2].x; + scaleY = components[2].y; + scaleZ = components[2].z; + } + + /** + * Sets interpolated value. + */ + public function interpolate(a:TransformKey, b:TransformKey, c:Number):void { + var c2:Number = 1 - c; + x = c2*a.x + c*b.x; + y = c2*a.y + c*b.y; + z = c2*a.z + c*b.z; + slerp(a.rotation, b.rotation, c, rotation); + scaleX = c2*a.scaleX + c*b.scaleX; + scaleY = c2*a.scaleY + c*b.scaleY; + scaleZ = c2*a.scaleZ + c*b.scaleZ; + } + + /** + * @private + * + * Performs spherical interpolation between two given quaternions by min distance + * + * @param a first quaternion. + * @param b second quaternion. + * @param t interpolation parameter, usually defines in [0, 1] range. + * @return this + */ + private function slerp(a:Vector3D, b:Vector3D, t:Number, result:Vector3D):void { + var flip:Number = 1; + // Since one orientation represents by two values q and -q, we should invert the sign of one of quaternions + // in case of negative value of the dot product. Otherwise the interpolation results by max distance. + var cosine:Number = a.w*b.w + a.x*b.x + a.y*b.y + a.z*b.z; + if (cosine < 0) { + cosine = -cosine; + flip = -1; + } + if ((1 - cosine) < 0.001) { + // Linear interpolation used near zero + var k1:Number = 1 - t; + var k2:Number = t*flip; + result.w = a.w*k1 + b.w*k2; + result.x = a.x*k1 + b.x*k2; + result.y = a.y*k1 + b.y*k2; + result.z = a.z*k1 + b.z*k2; + var d:Number = result.w*result.w + result.x*result.x + result.y*result.y + result.z*result.z; + if (d == 0) { + result.w = 1; + } else { + result.scaleBy(1/Math.sqrt(d)); + } + } else { + var theta:Number = Math.acos(cosine); + var sine:Number = Math.sin(theta); + var beta:Number = Math.sin((1 - t)*theta)/sine; + var alpha:Number = Math.sin(t*theta)/sine*flip; + result.w = a.w*beta + b.w*alpha; + result.x = a.x*beta + b.x*alpha; + result.y = a.y*beta + b.y*alpha; + result.z = a.z*beta + b.z*alpha; + } + } + + /** + * @inheritDoc + */ + override alternativa3d function get nextKeyFrame():Keyframe { + return next; + } + + /** + * @inheritDoc + */ + override alternativa3d function set nextKeyFrame(value:Keyframe):void { + next = TransformKey(value); + } + + } +} diff --git a/src/alternativa/engine3d/animation/keys/TransformTrack.as b/src/alternativa/engine3d/animation/keys/TransformTrack.as new file mode 100644 index 0000000..6552391 --- /dev/null +++ b/src/alternativa/engine3d/animation/keys/TransformTrack.as @@ -0,0 +1,240 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.animation.keys { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.animation.AnimationState; + + import flash.geom.Matrix3D; + import flash.geom.Orientation3D; + import flash.geom.Vector3D; + + use namespace alternativa3d; + + /** + * A track which animates object transformation. + */ + public class TransformTrack extends Track { + + private var keyList:TransformKey; + + private var lastKey:TransformKey; + + /** + * @private + */ + override alternativa3d function get keyFramesList():Keyframe { + return keyList; + } + + /** + * @private + */ + override alternativa3d function set keyFramesList(value:Keyframe):void { + keyList = TransformKey(value); + } + + + /** + * @private + */ + override alternativa3d function get lastKey():Keyframe { + return lastKey; + } + + /** + * @private + */ + override alternativa3d function set lastKey(value:Keyframe):void { + lastKey = TransformKey(value); + } + + /** + * Creates a TransformTrack object. + */ + public function TransformTrack(object:String) { + this.object = object; + } + + /** + * Adds new keyframe. Keyframes stores ordered by its time property. + * + * @param time time of the new keyframe. + * @param matrix value of property for the new keyframe. + * @return added keyframe. + */ + public function addKey(time:Number, matrix:Matrix3D):TransformKey { + var key:TransformKey = new TransformKey(); + key._time = time; + var components:Vector. = matrix.decompose(Orientation3D.QUATERNION); + key.x = components[0].x; + key.y = components[0].y; + key.z = components[0].z; + key.rotation = components[1]; + key.scaleX = components[2].x; + key.scaleY = components[2].y; + key.scaleZ = components[2].z; + addKeyToList(key); + return key; + } + + /** + * Adds new keyframe and initialize it by transformation components. + * Keyframes stores ordered by its time property. + * + * @param time time of the new keyframe. + * @return added keyframe. + */ + public function addKeyComponents(time:Number, x:Number = 0, y:Number = 0, z:Number = 0, rotationX:Number = 0, rotationY:Number = 0, rotationZ:Number = 0, scaleX:Number = 1, scaleY:Number = 1, scaleZ:Number = 1):TransformKey { + var key:TransformKey = new TransformKey(); + key._time = time; + key.x = x; + key.y = y; + key.z = z; + key.rotation = createQuatFromEuler(rotationX, rotationY, rotationZ); + key.scaleX = scaleX; + key.scaleY = scaleY; + key.scaleZ = scaleZ; + addKeyToList(key); + return key; + } + + /** + * @private + * + * Multiplies quat by additive from right: quat = quat * additive. + * + */ + private function appendQuat(quat:Vector3D, additive:Vector3D):void { + var ww:Number = additive.w*quat.w - additive.x*quat.x - additive.y*quat.y - additive.z*quat.z; + var xx:Number = additive.w*quat.x + additive.x*quat.w + additive.y*quat.z - additive.z*quat.y; + var yy:Number = additive.w*quat.y + additive.y*quat.w + additive.z*quat.x - additive.x*quat.z; + var zz:Number = additive.w*quat.z + additive.z*quat.w + additive.x*quat.y - additive.y*quat.x; + quat.w = ww; + quat.x = xx; + quat.y = yy; + quat.z = zz; + } + + /** + * @private + */ + private function normalizeQuat(quat:Vector3D):void { + var d:Number = quat.w*quat.w + quat.x*quat.x + quat.y*quat.y + quat.z*quat.z; + if (d == 0) { + quat.w = 1; + } else { + d = 1/Math.sqrt(d); + quat.w *= d; + quat.x *= d; + quat.y *= d; + quat.z *= d; + } + } + + /** + * @private + */ + private function setQuatFromAxisAngle(quat:Vector3D, x:Number, y:Number, z:Number, angle:Number):void { + quat.w = Math.cos(0.5*angle); + var k:Number = Math.sin(0.5*angle)/Math.sqrt(x*x + y*y + z*z); + quat.x = x*k; + quat.y = y*k; + quat.z = z*k; + } + + /** + * @private + */ + private static var tempQuat:Vector3D = new Vector3D(); + + /** + * @private + */ + private function createQuatFromEuler(x:Number, y:Number, z:Number):Vector3D { + var result:Vector3D = new Vector3D(); + setQuatFromAxisAngle(result, 1, 0, 0, x); + + setQuatFromAxisAngle(tempQuat, 0, 1, 0, y); + appendQuat(result, tempQuat); + normalizeQuat(result); + + setQuatFromAxisAngle(tempQuat, 0, 0, 1, z); + appendQuat(result, tempQuat); + normalizeQuat(result); + return result; + } + + /** + * @private + */ + private static var temp:TransformKey = new TransformKey(); + + private var recentKey:TransformKey = null; + + /** + * @private + */ + override alternativa3d function blend(time:Number, weight:Number, state:AnimationState):void { + var prev:TransformKey; + var next:TransformKey; + + if (recentKey != null && recentKey.time < time) { + prev = recentKey; + next = recentKey.next; + } else { + next = keyList; + } + while (next != null && next._time < time) { + prev = next; + next = next.next; + } + + if (prev != null) { + if (next != null) { + temp.interpolate(prev, next, (time - prev._time)/(next._time - prev._time)); + state.addWeightedTransform(temp, weight); + } else { + state.addWeightedTransform(prev, weight); + } + recentKey = prev; + } else { + if (next != null) { + state.addWeightedTransform(next, weight); + } + } + } + + /** + * @private + */ + override alternativa3d function createKeyFrame():Keyframe { + return new TransformKey(); + } + + /** + * @private + */ + override alternativa3d function interpolateKeyFrame(dest:Keyframe, a:Keyframe, b:Keyframe, value:Number):void { + TransformKey(dest).interpolate(TransformKey(a), TransformKey(b), value); + } + + /** + * @inheritDoc + */ + override public function slice(start:Number, end:Number = Number.MAX_VALUE):Track { + var track:TransformTrack = new TransformTrack(object); + sliceImplementation(track, start, end); + return track; + } + + } +} diff --git a/src/alternativa/engine3d/collisions/EllipsoidCollider.as b/src/alternativa/engine3d/collisions/EllipsoidCollider.as new file mode 100644 index 0000000..b0b19f3 --- /dev/null +++ b/src/alternativa/engine3d/collisions/EllipsoidCollider.as @@ -0,0 +1,576 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.collisions { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.*; + import alternativa.engine3d.resources.Geometry; + + import flash.geom.Vector3D; + import flash.utils.ByteArray; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * The class implements the algorithm of the continuous collision of an ellipsoid with the faces. + */ + public class EllipsoidCollider { + + /** + * Ellipsoid radius along X axis. + */ + public var radiusX:Number; + + /** + * Ellipsoid radius along Y axis. + */ + public var radiusY:Number; + + /** + * Ellipsoid radius along Z axis. + */ + public var radiusZ:Number; + + /** + * Geometric error. Minimum absolute difference between two values + * when they are considered to be different. Default value is 0.001. + */ + public var threshold:Number = 0.001; + + private var matrix:Transform3D = new Transform3D(); + private var inverseMatrix:Transform3D = new Transform3D(); + + /** + * @private + */ + alternativa3d var geometries:Vector. = new Vector.(); + /** + * @private + */ + alternativa3d var transforms:Vector. = new Vector.(); + + private var vertices:Vector. = new Vector.(); + private var normals:Vector. = new Vector.(); + private var indices:Vector. = new Vector.(); + private var numTriangles:int; + + private var radius:Number; + private var src:Vector3D = new Vector3D(); + private var displ:Vector3D = new Vector3D(); + private var dest:Vector3D = new Vector3D(); + + private var collisionPoint:Vector3D = new Vector3D(); + private var collisionPlane:Vector3D = new Vector3D(); + + /** + * @private + */ + alternativa3d var sphere:Vector3D = new Vector3D(); + private var cornerA:Vector3D = new Vector3D(); + private var cornerB:Vector3D = new Vector3D(); + private var cornerC:Vector3D = new Vector3D(); + private var cornerD:Vector3D = new Vector3D(); + + /** + * Creates a EllipsoidCollider object. + * + * @param radiusX Ellipsoid radius along X axis. + * @param radiusY Ellipsoid radius along Y axis. + * @param radiusZ Ellipsoid radius along Z axis. + */ + public function EllipsoidCollider(radiusX:Number, radiusY:Number, radiusZ:Number) { + this.radiusX = radiusX; + this.radiusY = radiusY; + this.radiusZ = radiusZ; + } + + /** + * @private + */ + alternativa3d function calculateSphere(transform:Transform3D):void { + sphere.x = transform.d; + sphere.y = transform.h; + sphere.z = transform.l; + var sax:Number = transform.a*cornerA.x + transform.b*cornerA.y + transform.c*cornerA.z + transform.d; + var say:Number = transform.e*cornerA.x + transform.f*cornerA.y + transform.g*cornerA.z + transform.h; + var saz:Number = transform.i*cornerA.x + transform.j*cornerA.y + transform.k*cornerA.z + transform.l; + var sbx:Number = transform.a*cornerB.x + transform.b*cornerB.y + transform.c*cornerB.z + transform.d; + var sby:Number = transform.e*cornerB.x + transform.f*cornerB.y + transform.g*cornerB.z + transform.h; + var sbz:Number = transform.i*cornerB.x + transform.j*cornerB.y + transform.k*cornerB.z + transform.l; + var scx:Number = transform.a*cornerC.x + transform.b*cornerC.y + transform.c*cornerC.z + transform.d; + var scy:Number = transform.e*cornerC.x + transform.f*cornerC.y + transform.g*cornerC.z + transform.h; + var scz:Number = transform.i*cornerC.x + transform.j*cornerC.y + transform.k*cornerC.z + transform.l; + var sdx:Number = transform.a*cornerD.x + transform.b*cornerD.y + transform.c*cornerD.z + transform.d; + var sdy:Number = transform.e*cornerD.x + transform.f*cornerD.y + transform.g*cornerD.z + transform.h; + var sdz:Number = transform.i*cornerD.x + transform.j*cornerD.y + transform.k*cornerD.z + transform.l; + var dx:Number = sax - sphere.x; + var dy:Number = say - sphere.y; + var dz:Number = saz - sphere.z; + sphere.w = dx*dx + dy*dy + dz*dz; + dx = sbx - sphere.x; + dy = sby - sphere.y; + dz = sbz - sphere.z; + var dxyz:Number = dx*dx + dy*dy + dz*dz; + if (dxyz > sphere.w) sphere.w = dxyz; + dx = scx - sphere.x; + dy = scy - sphere.y; + dz = scz - sphere.z; + dxyz = dx*dx + dy*dy + dz*dz; + if (dxyz > sphere.w) sphere.w = dxyz; + dx = sdx - sphere.x; + dy = sdy - sphere.y; + dz = sdz - sphere.z; + dxyz = dx*dx + dy*dy + dz*dz; + if (dxyz > sphere.w) sphere.w = dxyz; + sphere.w = Math.sqrt(sphere.w); + } + + private function prepare(source:Vector3D, displacement:Vector3D, object:Object3D, excludedObjects:Dictionary):void { + + // Radius of the sphere + radius = radiusX; + if (radiusY > radius) radius = radiusY; + if (radiusZ > radius) radius = radiusZ; + + // The matrix of the collider + matrix.compose(source.x, source.y, source.z, 0, 0, 0, radiusX/radius, radiusY/radius, radiusZ/radius); + inverseMatrix.copy(matrix); + inverseMatrix.invert(); + + // Local coordinates + src.x = 0; + src.y = 0; + src.z = 0; + // Local offset + displ.x = inverseMatrix.a*displacement.x + inverseMatrix.b*displacement.y + inverseMatrix.c*displacement.z; + displ.y = inverseMatrix.e*displacement.x + inverseMatrix.f*displacement.y + inverseMatrix.g*displacement.z; + displ.z = inverseMatrix.i*displacement.x + inverseMatrix.j*displacement.y + inverseMatrix.k*displacement.z; + // Local destination point + dest.x = src.x + displ.x; + dest.y = src.y + displ.y; + dest.z = src.z + displ.z; + + // Bound defined by movement of the sphere + var rad:Number = radius + displ.length; + cornerA.x = -rad; + cornerA.y = -rad; + cornerA.z = -rad; + cornerB.x = rad; + cornerB.y = -rad; + cornerB.z = -rad; + cornerC.x = rad; + cornerC.y = rad; + cornerC.z = -rad; + cornerD.x = -rad; + cornerD.y = rad; + cornerD.z = -rad; + + // Gathering the faces which with collision can occur + if (excludedObjects == null || !excludedObjects[object]) { + if (object.transformChanged) object.composeTransforms(); + object.globalToLocalTransform.combine(object.inverseTransform, matrix); + // Check collision with the bound + var intersects:Boolean = true; + if (object.boundBox != null) { + calculateSphere(object.globalToLocalTransform); + intersects = object.boundBox.checkSphere(sphere); + } + if (intersects) { + object.localToGlobalTransform.combine(inverseMatrix, object.transform); + object.collectGeometry(this, excludedObjects); + } + // Check children + if (object.childrenList != null) object.collectChildrenGeometry(this, excludedObjects); + } + + numTriangles = 0; + var indicesLength:int = 0; + var normalsLength:int = 0; + + // Loop geometries + var j:int; + var mapOffset:int = 0; + var verticesLength:int = 0; + var geometriesLength:int = geometries.length; + for (var i:int = 0; i < geometriesLength; i++) { + var geometry:Geometry = geometries[i]; + var transform:Transform3D = transforms[i]; + var geometryIndicesLength:int = geometry._indices.length; + if (geometry._numVertices == 0 || geometryIndicesLength == 0) continue; + // Transform vertices + var vBuffer:VertexStream = (VertexAttributes.POSITION < geometry._attributesStreams.length) ? geometry._attributesStreams[VertexAttributes.POSITION] : null; + if (vBuffer != null) { + var attributesOffset:int = geometry._attributesOffsets[VertexAttributes.POSITION]; + var numMappings:int = vBuffer.attributes.length; + var data:ByteArray = vBuffer.data; + for (j = 0; j < geometry._numVertices; j++) { + data.position = 4*(numMappings*j + attributesOffset); + var vx:Number = data.readFloat(); + var vy:Number = data.readFloat(); + var vz:Number = data.readFloat(); + vertices[verticesLength] = transform.a*vx + transform.b*vy + transform.c*vz + transform.d; verticesLength++; + vertices[verticesLength] = transform.e*vx + transform.f*vy + transform.g*vz + transform.h; verticesLength++; + vertices[verticesLength] = transform.i*vx + transform.j*vy + transform.k*vz + transform.l; verticesLength++; + } + } + // Loop triangles + var geometryIndices:Vector. = geometry._indices; + for (j = 0; j < geometryIndicesLength;) { + var a:int = geometryIndices[j] + mapOffset; j++; + var index:int = a*3; + var ax:Number = vertices[index]; index++; + var ay:Number = vertices[index]; index++; + var az:Number = vertices[index]; + var b:int = geometryIndices[j] + mapOffset; j++; + index = b*3; + var bx:Number = vertices[index]; index++; + var by:Number = vertices[index]; index++; + var bz:Number = vertices[index]; + var c:int = geometryIndices[j] + mapOffset; j++; + index = c*3; + var cx:Number = vertices[index]; index++; + var cy:Number = vertices[index]; index++; + var cz:Number = vertices[index]; + // Exclusion by bound + if (ax > rad && bx > rad && cx > rad || ax < -rad && bx < -rad && cx < -rad) continue; + if (ay > rad && by > rad && cy > rad || ay < -rad && by < -rad && cy < -rad) continue; + if (az > rad && bz > rad && cz > rad || az < -rad && bz < -rad && cz < -rad) continue; + // The normal + var abx:Number = bx - ax; + var aby:Number = by - ay; + var abz:Number = bz - az; + var acx:Number = cx - ax; + var acy:Number = cy - ay; + var acz:Number = cz - az; + var normalX:Number = acz*aby - acy*abz; + var normalY:Number = acx*abz - acz*abx; + var normalZ:Number = acy*abx - acx*aby; + var len:Number = normalX*normalX + normalY*normalY + normalZ*normalZ; + if (len < 0.001) continue; + len = 1/Math.sqrt(len); + normalX *= len; + normalY *= len; + normalZ *= len; + var offset:Number = ax*normalX + ay*normalY + az*normalZ; + if (offset > rad || offset < -rad) continue; + indices[indicesLength] = a; indicesLength++; + indices[indicesLength] = b; indicesLength++; + indices[indicesLength] = c; indicesLength++; + normals[normalsLength] = normalX; normalsLength++; + normals[normalsLength] = normalY; normalsLength++; + normals[normalsLength] = normalZ; normalsLength++; + normals[normalsLength] = offset; normalsLength++; + numTriangles++; + } + // Offset by nomber of vertices + mapOffset += geometry._numVertices; + } + geometries.length = 0; + transforms.length = 0; + } + + /** + * Calculates destination point from given start position and displacement vector. + * @param source Starting point. + * @param displacement Displacement vector. + * @param object An object at crossing which will be checked. If this is a container, the application will participate and its child objects + * @param excludedObjects An associative array whose keys are instances of Object3D and its children. + * The objects that are keys of this dictionary will be excluded from intersection test. + * @return Destination point. + */ + public function calculateDestination(source:Vector3D, displacement:Vector3D, object:Object3D, excludedObjects:Dictionary = null):Vector3D { + + if (displacement.length <= threshold) return source.clone(); + + prepare(source, displacement, object, excludedObjects); + + if (numTriangles > 0) { + var limit:int = 50; + for (var i:int = 0; i < limit; i++) { + if (checkCollision()) { + // Offset destination point from behind collision plane by radius of the sphere over plane, along the normal + var offset:Number = radius + threshold + collisionPlane.w - dest.x*collisionPlane.x - dest.y*collisionPlane.y - dest.z*collisionPlane.z; + dest.x += collisionPlane.x*offset; + dest.y += collisionPlane.y*offset; + dest.z += collisionPlane.z*offset; + // Fixing up the current sphere coordinates for the next iteration + src.x = collisionPoint.x + collisionPlane.x*(radius + threshold); + src.y = collisionPoint.y + collisionPlane.y*(radius + threshold); + src.z = collisionPoint.z + collisionPlane.z*(radius + threshold); + // Fixing up velocity vector. The result ordered along plane of collision. + displ.x = dest.x - src.x; + displ.y = dest.y - src.y; + displ.z = dest.z - src.z; + if (displ.length < threshold) break; + } else break; + } + // Setting the coordinates + return new Vector3D(matrix.a*dest.x + matrix.b*dest.y + matrix.c*dest.z + matrix.d, matrix.e*dest.x + matrix.f*dest.y + matrix.g*dest.z + matrix.h, matrix.i*dest.x + matrix.j*dest.y + matrix.k*dest.z + matrix.l); + } else { + return new Vector3D(source.x + displacement.x, source.y + displacement.y, source.z + displacement.z); + } + } + + /** + * Finds first collision from given starting point aling displacement vector. + * @param source Starting point. + * @param displacement Displacement vector. + * @param resCollisionPoint Collision point will be written into this variable. + * @param resCollisionPlane Collision plane (defines by normal) parameters will be written into this variable. + * @param object The object to use in collision detection. If a container is specified, all its children will be tested for collison with ellipsoid. + * @param excludedObjects An associative array whose keys are instances of Object3D and its children. + * @return true if collision detected and false otherwise. + */ + public function getCollision(source:Vector3D, displacement:Vector3D, resCollisionPoint:Vector3D, resCollisionPlane:Vector3D, object:Object3D, excludedObjects:Dictionary = null):Boolean { + + if (displacement.length <= threshold) return false; + + prepare(source, displacement, object, excludedObjects); + + if (numTriangles > 0) { + if (checkCollision()) { + + // Transform the point to the global space + resCollisionPoint.x = matrix.a*collisionPoint.x + matrix.b*collisionPoint.y + matrix.c*collisionPoint.z + matrix.d; + resCollisionPoint.y = matrix.e*collisionPoint.x + matrix.f*collisionPoint.y + matrix.g*collisionPoint.z + matrix.h; + resCollisionPoint.z = matrix.i*collisionPoint.x + matrix.j*collisionPoint.y + matrix.k*collisionPoint.z + matrix.l; + + // Transform the plane to the global space + var abx:Number; + var aby:Number; + var abz:Number; + if (collisionPlane.x < collisionPlane.y) { + if (collisionPlane.x < collisionPlane.z) { + abx = 0; + aby = -collisionPlane.z; + abz = collisionPlane.y; + } else { + abx = -collisionPlane.y; + aby = collisionPlane.x; + abz = 0; + } + } else { + if (collisionPlane.y < collisionPlane.z) { + abx = collisionPlane.z; + aby = 0; + abz = -collisionPlane.x; + } else { + abx = -collisionPlane.y; + aby = collisionPlane.x; + abz = 0; + } + } + var acx:Number = collisionPlane.z*aby - collisionPlane.y*abz; + var acy:Number = collisionPlane.x*abz - collisionPlane.z*abx; + var acz:Number = collisionPlane.y*abx - collisionPlane.x*aby; + + var abx2:Number = matrix.a*abx + matrix.b*aby + matrix.c*abz; + var aby2:Number = matrix.e*abx + matrix.f*aby + matrix.g*abz; + var abz2:Number = matrix.i*abx + matrix.j*aby + matrix.k*abz; + var acx2:Number = matrix.a*acx + matrix.b*acy + matrix.c*acz; + var acy2:Number = matrix.e*acx + matrix.f*acy + matrix.g*acz; + var acz2:Number = matrix.i*acx + matrix.j*acy + matrix.k*acz; + + resCollisionPlane.x = abz2*acy2 - aby2*acz2; + resCollisionPlane.y = abx2*acz2 - abz2*acx2; + resCollisionPlane.z = aby2*acx2 - abx2*acy2; + resCollisionPlane.normalize(); + resCollisionPlane.w = resCollisionPoint.x*resCollisionPlane.x + resCollisionPoint.y*resCollisionPlane.y + resCollisionPoint.z*resCollisionPlane.z; + + return true; + } else { + return false; + } + } + return false; + } + + private function checkCollision():Boolean { + var minTime:Number = 1; + var displacementLength:Number = displ.length; + // Loop triangles + var indicesLength:int = numTriangles*3; + for (var i:int = 0, j:int = 0; i < indicesLength;) { + // Points + var index:int = indices[i]*3; i++; + var ax:Number = vertices[index]; index++; + var ay:Number = vertices[index]; index++; + var az:Number = vertices[index]; + index = indices[i]*3; i++; + var bx:Number = vertices[index]; index++; + var by:Number = vertices[index]; index++; + var bz:Number = vertices[index]; + index = indices[i]*3; i++; + var cx:Number = vertices[index]; index++; + var cy:Number = vertices[index]; index++; + var cz:Number = vertices[index]; + // Normal + var normalX:Number = normals[j]; j++; + var normalY:Number = normals[j]; j++; + var normalZ:Number = normals[j]; j++; + var offset:Number = normals[j]; j++; + var distance:Number = src.x*normalX + src.y*normalY + src.z*normalZ - offset; + // The intersection of plane and sphere + var pointX:Number; + var pointY:Number; + var pointZ:Number; + if (distance < radius) { + pointX = src.x - normalX*distance; + pointY = src.y - normalY*distance; + pointZ = src.z - normalZ*distance; + } else { + var t:Number = (distance - radius)/(distance - dest.x*normalX - dest.y*normalY - dest.z*normalZ + offset); + pointX = src.x + displ.x*t - normalX*radius; + pointY = src.y + displ.y*t - normalY*radius; + pointZ = src.z + displ.z*t - normalZ*radius; + } + // Closest polygon vertex + var faceX:Number; + var faceY:Number; + var faceZ:Number; + var min:Number = 1e+22; + // Loop edges + var inside:Boolean = true; + for (var k:int = 0; k < 3; k++) { + var p1x:Number; + var p1y:Number; + var p1z:Number; + var p2x:Number; + var p2y:Number; + var p2z:Number; + if (k == 0) { + p1x = ax; + p1y = ay; + p1z = az; + p2x = bx; + p2y = by; + p2z = bz; + } else if (k == 1) { + p1x = bx; + p1y = by; + p1z = bz; + p2x = cx; + p2y = cy; + p2z = cz; + } else { + p1x = cx; + p1y = cy; + p1z = cz; + p2x = ax; + p2y = ay; + p2z = az; + } + var abx:Number = p2x - p1x; + var aby:Number = p2y - p1y; + var abz:Number = p2z - p1z; + var acx:Number = pointX - p1x; + var acy:Number = pointY - p1y; + var acz:Number = pointZ - p1z; + var crx:Number = acz*aby - acy*abz; + var cry:Number = acx*abz - acz*abx; + var crz:Number = acy*abx - acx*aby; + // Case of the point is outside of the polygon + if (crx*normalX + cry*normalY + crz*normalZ < 0) { + var edgeLength:Number = abx*abx + aby*aby + abz*abz; + var edgeDistanceSqr:Number = (crx*crx + cry*cry + crz*crz)/edgeLength; + if (edgeDistanceSqr < min) { + // Edge normalization + edgeLength = Math.sqrt(edgeLength); + abx /= edgeLength; + aby /= edgeLength; + abz /= edgeLength; + // Distance to intersecion of normal along theedge + t = abx*acx + aby*acy + abz*acz; + var acLen:Number; + if (t < 0) { + // Closest point is the first one + acLen = acx*acx + acy*acy + acz*acz; + if (acLen < min) { + min = acLen; + faceX = p1x; + faceY = p1y; + faceZ = p1z; + } + } else if (t > edgeLength) { + // Closest point is the second one + acx = pointX - p2x; + acy = pointY - p2y; + acz = pointZ - p2z; + acLen = acx*acx + acy*acy + acz*acz; + if (acLen < min) { + min = acLen; + faceX = p2x; + faceY = p2y; + faceZ = p2z; + } + } else { + // Closest point is on edge + min = edgeDistanceSqr; + faceX = p1x + abx*t; + faceY = p1y + aby*t; + faceZ = p1z + abz*t; + } + } + inside = false; + } + } + // Case of point is inside polygon + if (inside) { + faceX = pointX; + faceY = pointY; + faceZ = pointZ; + } + // Vector pointed from closest point to the center of sphere + var deltaX:Number = src.x - faceX; + var deltaY:Number = src.y - faceY; + var deltaZ:Number = src.z - faceZ; + // If movement directed to point + if (deltaX*displ.x + deltaY*displ.y + deltaZ*displ.z <= 0) { + // reversed vector + var backX:Number = -displ.x/displacementLength; + var backY:Number = -displ.y/displacementLength; + var backZ:Number = -displ.z/displacementLength; + // Length of Vector pointed from closest point to the center of sphere + var deltaLength:Number = deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ; + // Projection Vector pointed from closest point to the center of sphere on reversed vector + var projectionLength:Number = deltaX*backX + deltaY*backY + deltaZ*backZ; + var projectionInsideLength:Number = radius*radius - deltaLength + projectionLength*projectionLength; + if (projectionInsideLength > 0) { + // Time of the intersection + var time:Number = (projectionLength - Math.sqrt(projectionInsideLength))/displacementLength; + // Collision with closest point occurs + if (time < minTime) { + minTime = time; + collisionPoint.x = faceX; + collisionPoint.y = faceY; + collisionPoint.z = faceZ; + if (inside) { + collisionPlane.x = normalX; + collisionPlane.y = normalY; + collisionPlane.z = normalZ; + collisionPlane.w = offset; + } else { + deltaLength = Math.sqrt(deltaLength); + collisionPlane.x = deltaX/deltaLength; + collisionPlane.y = deltaY/deltaLength; + collisionPlane.z = deltaZ/deltaLength; + collisionPlane.w = collisionPoint.x*collisionPlane.x + collisionPoint.y*collisionPlane.y + collisionPoint.z*collisionPlane.z; + } + } + } + } + } + return minTime < 1; + } + + } +} diff --git a/src/alternativa/engine3d/controllers/SimpleObjectController.as b/src/alternativa/engine3d/controllers/SimpleObjectController.as new file mode 100644 index 0000000..99533e6 --- /dev/null +++ b/src/alternativa/engine3d/controllers/SimpleObjectController.as @@ -0,0 +1,486 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.controllers { + + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Object3D; + + import flash.display.InteractiveObject; + import flash.events.KeyboardEvent; + import flash.events.MouseEvent; + import flash.geom.Matrix3D; + import flash.geom.Point; + import flash.geom.Vector3D; + import flash.ui.Keyboard; + import flash.utils.getTimer; + + /** + * Controller for Object3D. Allow to handle the object with a keyboard and mouse. + * + * @see alternativa.engine3d.core.Object3D + */ + public class SimpleObjectController { + + /** + * Name of action for binding "forward" action. + */ + public static const ACTION_FORWARD:String = "ACTION_FORWARD"; + + /** + * Name of action for binding "back" action. + */ + public static const ACTION_BACK:String = "ACTION_BACK"; + + /** + * Name of action for binding "left" action. + */ + public static const ACTION_LEFT:String = "ACTION_LEFT"; + + /** + * Name of action for binding "right" action. + */ + public static const ACTION_RIGHT:String = "ACTION_RIGHT"; + + /** + * Name of action for binding "up" action. + */ + public static const ACTION_UP:String = "ACTION_UP"; + + /** + * Name of action for binding "down" action. + */ + public static const ACTION_DOWN:String = "ACTION_DOWN"; + + /** + * Name of action for binding "pitch up" action. + */ + public static const ACTION_PITCH_UP:String = "ACTION_PITCH_UP"; + + /** + * Name of action for binding "pitch down" action. + */ + public static const ACTION_PITCH_DOWN:String = "ACTION_PITCH_DOWN"; + + /** + * Name of action for binding "yaw left" action. + */ + public static const ACTION_YAW_LEFT:String = "ACTION_YAW_LEFT"; + + /** + * Name of action for binding "yaw right" action. + */ + public static const ACTION_YAW_RIGHT:String = "ACTION_YAW_RIGHT"; + + /** + * Name of action for binding "accelerate" action. + */ + public static const ACTION_ACCELERATE:String = "ACTION_ACCELERATE"; + + /** + * ИName of action for binding "mouse look" action. + */ + public static const ACTION_MOUSE_LOOK:String = "ACTION_MOUSE_LOOK"; + + /** + * Speed. + */ + public var speed:Number; + + /** + * Speed multiplier for acceleration mode. + */ + public var speedMultiplier:Number; + + /** + * Mouse sensitivity. + */ + public var mouseSensitivity:Number; + + /** + * The maximal slope in the vertical plane in radians. + */ + public var maxPitch:Number = 1e+22; + + /** + * The minimal slope in the vertical plane in radians. + */ + public var minPitch:Number = -1e+22; + + private var eventSource:InteractiveObject; + private var _object:Object3D; + + private var _up:Boolean; + private var _down:Boolean; + private var _forward:Boolean; + private var _back:Boolean; + private var _left:Boolean; + private var _right:Boolean; + private var _accelerate:Boolean; + + private var displacement:Vector3D = new Vector3D(); + private var mousePoint:Point = new Point(); + private var mouseLook:Boolean; + private var objectTransform:Vector.; + + private var time:int; + + /** + * The hash for binding names of action and functions. The functions should be at a form are follows: + * + * function(value:Boolean):void + * + * + * value argument defines if bound key pressed down or up. + */ + private var actionBindings:Object = {}; + + /** + * The hash for binding key codes and action names. + */ + protected var keyBindings:Object = {}; + + /** + * Creates a SimpleObjectController object. + * @param eventSource Source for event listening. + * @param speed Speed of movement. + * @param mouseSensitivity Mouse sensitivity, i.e. number of degrees per each pixel of mouse movement. + */ + public function SimpleObjectController(eventSource:InteractiveObject, object:Object3D, speed:Number, speedMultiplier:Number = 3, mouseSensitivity:Number = 1) { + this.eventSource = eventSource; + this.object = object; + this.speed = speed; + this.speedMultiplier = speedMultiplier; + this.mouseSensitivity = mouseSensitivity; + + actionBindings[ACTION_FORWARD] = moveForward; + actionBindings[ACTION_BACK] = moveBack; + actionBindings[ACTION_LEFT] = moveLeft; + actionBindings[ACTION_RIGHT] = moveRight; + actionBindings[ACTION_UP] = moveUp; + actionBindings[ACTION_DOWN] = moveDown; + actionBindings[ACTION_ACCELERATE] = accelerate; + + setDefaultBindings(); + + enable(); + } + + /** + * Enables the controler. + */ + public function enable():void { + eventSource.addEventListener(KeyboardEvent.KEY_DOWN, onKey); + eventSource.addEventListener(KeyboardEvent.KEY_UP, onKey); + eventSource.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); + eventSource.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); + } + + /** + * Disables the controller. + */ + public function disable():void { + eventSource.removeEventListener(KeyboardEvent.KEY_DOWN, onKey); + eventSource.removeEventListener(KeyboardEvent.KEY_UP, onKey); + eventSource.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); + eventSource.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp); + stopMouseLook(); + } + + private function onMouseDown(e:MouseEvent):void { + startMouseLook(); + } + + private function onMouseUp(e:MouseEvent):void { + stopMouseLook(); + } + + /** + * Enables mouse look mode. + */ + public function startMouseLook():void { + mousePoint.x = eventSource.mouseX; + mousePoint.y = eventSource.mouseY; + mouseLook = true; + } + + /** + * Disables mouse look mode. + */ + public function stopMouseLook():void { + mouseLook = false; + } + + private function onKey(e:KeyboardEvent):void { + var method:Function = keyBindings[e.keyCode]; + if (method != null) method.call(this, e.type == KeyboardEvent.KEY_DOWN); + } + + /** + * Target of handling. + */ + public function get object():Object3D { + return _object; + } + + /** + * @private + */ + public function set object(value:Object3D):void { + _object = value; + updateObjectTransform(); + } + + /** + * Refreshes controller state from state of handled object. Should be called if object was moved without the controller (i.e. object.x = 100;). + */ + public function updateObjectTransform():void { + if (_object != null) objectTransform = _object.matrix.decompose(); + } + + /** + * Calculates and sets new object position. + */ + public function update():void { + if (_object == null) return; + + var frameTime:Number = time; + time = getTimer(); + frameTime = 0.001*(time - frameTime); + if (frameTime > 0.1) frameTime = 0.1; + + var moved:Boolean = false; + + if (mouseLook) { + var dx:Number = eventSource.mouseX - mousePoint.x; + var dy:Number = eventSource.mouseY - mousePoint.y; + mousePoint.x = eventSource.mouseX; + mousePoint.y = eventSource.mouseY; + var v:Vector3D = objectTransform[1]; + v.x -= dy*Math.PI/180*mouseSensitivity; + if (v.x > maxPitch) v.x = maxPitch; + if (v.x < minPitch) v.x = minPitch; + v.z -= dx*Math.PI/180*mouseSensitivity; + moved = true; + } + + displacement.x = _right ? 1 : (_left ? -1 : 0); + displacement.y = _forward ? 1 : (_back ? -1 : 0); + displacement.z = _up ? 1 : (_down ? -1 : 0); + if (displacement.lengthSquared > 0) { + if (_object is Camera3D) { + var tmp:Number = displacement.z; + displacement.z = displacement.y; + displacement.y = -tmp; + } + deltaTransformVector(displacement); + if (_accelerate) displacement.scaleBy(speedMultiplier*speed*frameTime/displacement.length); + else displacement.scaleBy(speed*frameTime/displacement.length); + (objectTransform[0] as Vector3D).incrementBy(displacement); + moved = true; + } + + if (moved) { + var m:Matrix3D = new Matrix3D(); + m.recompose(objectTransform); + _object.matrix = m; + } + } + + /** + * Sets object at given position. + * @param pos The position. + */ + public function setObjectPos(pos:Vector3D):void { + if (_object != null) { + var v:Vector3D = objectTransform[0]; + v.x = pos.x; + v.y = pos.y; + v.z = pos.z; + } + } + + /** + * Sets object at given position. + * @param x X. + * @param y Y. + * @param z Z. + */ + public function setObjectPosXYZ(x:Number, y:Number, z:Number):void { + if (_object != null) { + var v:Vector3D = objectTransform[0]; + v.x = x; + v.y = y; + v.z = z; + } + } + + /** + * Sets direction of Z-axis of handled object to pointed at given place. If object is a camera, it will look to this direction. + * @param point Point to look at. + */ + public function lookAt(point:Vector3D):void { + lookAtXYZ(point.x, point.y, point.z); + } + + /** + * Sets direction of Z-axis of handled object to pointed at given place. If object is a camera, it will look to this direction. + * @param x X. + * @param y Y. + * @param z Z. + */ + public function lookAtXYZ(x:Number, y:Number, z:Number):void { + if (_object == null) return; + var v:Vector3D = objectTransform[0]; + var dx:Number = x - v.x; + var dy:Number = y - v.y; + var dz:Number = z - v.z; + v = objectTransform[1]; + v.x = Math.atan2(dz, Math.sqrt(dx*dx + dy*dy)); + if (_object is Camera3D) v.x -= 0.5*Math.PI; + v.y = 0; + v.z = -Math.atan2(dx, dy); + var m:Matrix3D = _object.matrix; + m.recompose(objectTransform); + _object.matrix = m; + } + + private var _vin:Vector. = new Vector.(3); + private var _vout:Vector. = new Vector.(3); + + private function deltaTransformVector(v:Vector3D):void { + _vin[0] = v.x; + _vin[1] = v.y; + _vin[2] = v.z; + _object.matrix.transformVectors(_vin, _vout); + var c:Vector3D = objectTransform[0]; + v.x = _vout[0] - c.x; + v.y = _vout[1] - c.y; + v.z = _vout[2] - c.z; + } + + /** + * Starts and stops move forward according to true or false was passed. + * @param value Action switcher. + */ + public function moveForward(value:Boolean):void { + _forward = value; + } + + /** + * Starts and stops move backward according to true or false was passed. + * @param value Action switcher. + */ + public function moveBack(value:Boolean):void { + _back = value; + } + + /** + * Starts and stops move to left according to true or false was passed. + * @param value Action switcher. + */ + public function moveLeft(value:Boolean):void { + _left = value; + } + + /** + * Starts and stops move to right according to true or false was passed. + * @param value Action switcher. + */ + public function moveRight(value:Boolean):void { + _right = value; + } + + /** + * Starts and stops move up according to true or false was passed. + * @param value Action switcher. + */ + public function moveUp(value:Boolean):void { + _up = value; + } + + /** + * Starts and stops move down according to true or false was passed. + * @param value Action switcher. + */ + public function moveDown(value:Boolean):void { + _down = value; + } + + /** + * Switches acceleration mode. + * @param value true turns acceleration on, false turns off. + */ + public function accelerate(value:Boolean):void { + _accelerate = value; + } + + /** + * Binds key and action. Only one action can be assigned to one key. + * @param keyCode Key code. + * @param action Action name. + * @see #unbindKey() + * @see #unbindAll() + */ + public function bindKey(keyCode:uint, action:String):void { + var method:Function = actionBindings[action]; + if (method != null) keyBindings[keyCode] = method; + } + + /** + * Binds keys and actions. Only one action can be assigned to one key. + * @param bindings Array which consists of sequence of couples of key code and action. An example are follows: [ keyCode1, action1, keyCode2, action2 ] . + */ + public function bindKeys(bindings:Array):void { + for (var i:int = 0; i < bindings.length; i += 2) bindKey(bindings[i], bindings[i + 1]); + } + + /** + * Clear binding for given keyCode. + * @param keyCode Key code. + * @see #bindKey() + * @see #unbindAll() + */ + public function unbindKey(keyCode:uint):void { + delete keyBindings[keyCode]; + } + + /** + * Clear binding of all keys. + * @see #bindKey() + * @see #unbindKey() + */ + public function unbindAll():void { + for (var key:String in keyBindings) delete keyBindings[key]; + } + + /** + * Sets default binding. + * @see #bindKey() + * @see #unbindKey() + * @see #unbindAll() + */ + public function setDefaultBindings():void { + bindKey(87, ACTION_FORWARD); + bindKey(83, ACTION_BACK); + bindKey(65, ACTION_LEFT); + bindKey(68, ACTION_RIGHT); + bindKey(69, ACTION_UP); + bindKey(67, ACTION_DOWN); + bindKey(Keyboard.SHIFT, ACTION_ACCELERATE); + + bindKey(Keyboard.UP, ACTION_FORWARD); + bindKey(Keyboard.DOWN, ACTION_BACK); + bindKey(Keyboard.LEFT, ACTION_LEFT); + bindKey(Keyboard.RIGHT, ACTION_RIGHT); + } + + } +} diff --git a/src/alternativa/engine3d/core/BoundBox.as b/src/alternativa/engine3d/core/BoundBox.as new file mode 100644 index 0000000..a90134a --- /dev/null +++ b/src/alternativa/engine3d/core/BoundBox.as @@ -0,0 +1,299 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + + import flash.geom.Vector3D; + + use namespace alternativa3d; + + /** + * Class stores object's bounding box object's local space. Generally, position of child objects isn't considered at BoundBox calculation. + * Ray intersection always made boundBox check at first, but it's possible to check on crossing boundBox only. + * + */ + public class BoundBox { + /** + * Left face. + */ + public var minX:Number = 1e+22; + /** + * Back face. + */ + public var minY:Number = 1e+22; + /** + * Bottom face. + */ + public var minZ:Number = 1e+22; + /** + * Right face. + */ + public var maxX:Number = -1e+22; + /** + * Ftont face. + */ + public var maxY:Number = -1e+22; + /** + * Top face. + */ + public var maxZ:Number = -1e+22; + + + /** + * Resets all bounds values to its initial state. + */ + public function reset():void { + minX = 1e+22; + minY = 1e+22; + minZ = 1e+22; + maxX = -1e+22; + maxY = -1e+22; + maxZ = -1e+22; + } + + /** + * @private + */ + alternativa3d function checkFrustumCulling(frustum:CullingPlane, culling:int):int { + var side:int = 1; + for (var plane:CullingPlane = frustum; plane != null; plane = plane.next) { + if (culling & side) { + if (plane.x >= 0) if (plane.y >= 0) if (plane.z >= 0) { + if (maxX*plane.x + maxY*plane.y + maxZ*plane.z <= plane.offset) return -1; + if (minX*plane.x + minY*plane.y + minZ*plane.z > plane.offset) culling &= (63 & ~side); + } else { + if (maxX*plane.x + maxY*plane.y + minZ*plane.z <= plane.offset) return -1; + if (minX*plane.x + minY*plane.y + maxZ*plane.z > plane.offset) culling &= (63 & ~side); + } else if (plane.z >= 0) { + if (maxX*plane.x + minY*plane.y + maxZ*plane.z <= plane.offset) return -1; + if (minX*plane.x + maxY*plane.y + minZ*plane.z > plane.offset) culling &= (63 & ~side); + } else { + if (maxX*plane.x + minY*plane.y + minZ*plane.z <= plane.offset) return -1; + if (minX*plane.x + maxY*plane.y + maxZ*plane.z > plane.offset) culling &= (63 & ~side); + } else if (plane.y >= 0) if (plane.z >= 0) { + if (minX*plane.x + maxY*plane.y + maxZ*plane.z <= plane.offset) return -1; + if (maxX*plane.x + minY*plane.y + minZ*plane.z > plane.offset) culling &= (63 & ~side); + } else { + if (minX*plane.x + maxY*plane.y + minZ*plane.z <= plane.offset) return -1; + if (maxX*plane.x + minY*plane.y + maxZ*plane.z > plane.offset) culling &= (63 & ~side); + } else if (plane.z >= 0) { + if (minX*plane.x + minY*plane.y + maxZ*plane.z <= plane.offset) return -1; + if (maxX*plane.x + maxY*plane.y + minZ*plane.z > plane.offset) culling &= (63 & ~side); + } else { + if (minX*plane.x + minY*plane.y + minZ*plane.z <= plane.offset) return -1; + if (maxX*plane.x + maxY*plane.y + maxZ*plane.z > plane.offset) culling &= (63 & ~side); + } + } + side <<= 1; + } + return culling; + } + + /** + * @private + */ + alternativa3d function checkOcclusion(occluders:Vector., occludersLength:int, transform:Transform3D):Boolean { + var ax:Number = transform.a*minX + transform.b*minY + transform.c*minZ + transform.d; + var ay:Number = transform.e*minX + transform.f*minY + transform.g*minZ + transform.h; + var az:Number = transform.i*minX + transform.j*minY + transform.k*minZ + transform.l; + var bx:Number = transform.a*maxX + transform.b*minY + transform.c*minZ + transform.d; + var by:Number = transform.e*maxX + transform.f*minY + transform.g*minZ + transform.h; + var bz:Number = transform.i*maxX + transform.j*minY + transform.k*minZ + transform.l; + var cx:Number = transform.a*minX + transform.b*maxY + transform.c*minZ + transform.d; + var cy:Number = transform.e*minX + transform.f*maxY + transform.g*minZ + transform.h; + var cz:Number = transform.i*minX + transform.j*maxY + transform.k*minZ + transform.l; + var dx:Number = transform.a*maxX + transform.b*maxY + transform.c*minZ + transform.d; + var dy:Number = transform.e*maxX + transform.f*maxY + transform.g*minZ + transform.h; + var dz:Number = transform.i*maxX + transform.j*maxY + transform.k*minZ + transform.l; + var ex:Number = transform.a*minX + transform.b*minY + transform.c*maxZ + transform.d; + var ey:Number = transform.e*minX + transform.f*minY + transform.g*maxZ + transform.h; + var ez:Number = transform.i*minX + transform.j*minY + transform.k*maxZ + transform.l; + var fx:Number = transform.a*maxX + transform.b*minY + transform.c*maxZ + transform.d; + var fy:Number = transform.e*maxX + transform.f*minY + transform.g*maxZ + transform.h; + var fz:Number = transform.i*maxX + transform.j*minY + transform.k*maxZ + transform.l; + var gx:Number = transform.a*minX + transform.b*maxY + transform.c*maxZ + transform.d; + var gy:Number = transform.e*minX + transform.f*maxY + transform.g*maxZ + transform.h; + var gz:Number = transform.i*minX + transform.j*maxY + transform.k*maxZ + transform.l; + var hx:Number = transform.a*maxX + transform.b*maxY + transform.c*maxZ + transform.d; + var hy:Number = transform.e*maxX + transform.f*maxY + transform.g*maxZ + transform.h; + var hz:Number = transform.i*maxX + transform.j*maxY + transform.k*maxZ + transform.l; + for (var i:int = 0; i < occludersLength; i++) { + var occluder:Occluder = occluders[i]; + for (var plane:CullingPlane = occluder.planeList; plane != null; plane = plane.next) { + if (plane.x*ax + plane.y*ay + plane.z*az > plane.offset || + plane.x*bx + plane.y*by + plane.z*bz > plane.offset || + plane.x*cx + plane.y*cy + plane.z*cz > plane.offset || + plane.x*dx + plane.y*dy + plane.z*dz > plane.offset || + plane.x*ex + plane.y*ey + plane.z*ez > plane.offset || + plane.x*fx + plane.y*fy + plane.z*fz > plane.offset || + plane.x*gx + plane.y*gy + plane.z*gz > plane.offset || + plane.x*hx + plane.y*hy + plane.z*hz > plane.offset) break; + } + if (plane == null) return true; + } + return false; + } + + /** + * @private + */ + alternativa3d function checkRays(origins:Vector., directions:Vector., raysLength:int):Boolean { + for (var i:int = 0; i < raysLength; i++) { + var origin:Vector3D = origins[i]; + var direction:Vector3D = directions[i]; + if (origin.x >= minX && origin.x <= maxX && origin.y >= minY && origin.y <= maxY && origin.z >= minZ && origin.z <= maxZ) return true; + if (origin.x < minX && direction.x <= 0 || origin.x > maxX && direction.x >= 0 || origin.y < minY && direction.y <= 0 || origin.y > maxY && direction.y >= 0 || origin.z < minZ && direction.z <= 0 || origin.z > maxZ && direction.z >= 0) continue; + var a:Number; + var b:Number; + var c:Number; + var d:Number; + var threshold:Number = 0.000001; + // Intersection of X and Y projection + if (direction.x > threshold) { + a = (minX - origin.x)/direction.x; + b = (maxX - origin.x)/direction.x; + } else if (direction.x < -threshold) { + a = (maxX - origin.x)/direction.x; + b = (minX - origin.x)/direction.x; + } else { + a = 0; + b = 1e+22; + } + if (direction.y > threshold) { + c = (minY - origin.y)/direction.y; + d = (maxY - origin.y)/direction.y; + } else if (direction.y < -threshold) { + c = (maxY - origin.y)/direction.y; + d = (minY - origin.y)/direction.y; + } else { + c = 0; + d = 1e+22; + } + if (c >= b || d <= a) continue; + if (c < a) { + if (d < b) b = d; + } else { + a = c; + if (d < b) b = d; + } + // Intersection of XY and Z projections + if (direction.z > threshold) { + c = (minZ - origin.z)/direction.z; + d = (maxZ - origin.z)/direction.z; + } else if (direction.z < -threshold) { + c = (maxZ - origin.z)/direction.z; + d = (minZ - origin.z)/direction.z; + } else { + c = 0; + d = 1e+22; + } + if (c >= b || d <= a) continue; + return true; + } + return false; + } + + /** + * @private + */ + alternativa3d function checkSphere(sphere:Vector3D):Boolean { + return sphere.x + sphere.w > minX && sphere.x - sphere.w < maxX && sphere.y + sphere.w > minY && sphere.y - sphere.w < maxY && sphere.z + sphere.w > minZ && sphere.z - sphere.w < maxZ; + } + + /** + * Checks if the ray crosses the BoundBox. + * + * @param origin Ray origin. + * @param direction Ray direction. + * @return true if intersection was found and false otherwise. + */ + public function intersectRay(origin:Vector3D, direction:Vector3D):Boolean { + if (origin.x >= minX && origin.x <= maxX && origin.y >= minY && origin.y <= maxY && origin.z >= minZ && origin.z <= maxZ) return true; + if (origin.x < minX && direction.x <= 0) return false; + if (origin.x > maxX && direction.x >= 0) return false; + if (origin.y < minY && direction.y <= 0) return false; + if (origin.y > maxY && direction.y >= 0) return false; + if (origin.z < minZ && direction.z <= 0) return false; + if (origin.z > maxZ && direction.z >= 0) return false; + var a:Number; + var b:Number; + var c:Number; + var d:Number; + var threshold:Number = 0.000001; + // Intersection of X and Y projection + if (direction.x > threshold) { + a = (minX - origin.x) / direction.x; + b = (maxX - origin.x) / direction.x; + } else if (direction.x < -threshold) { + a = (maxX - origin.x) / direction.x; + b = (minX - origin.x) / direction.x; + } else { + a = -1e+22; + b = 1e+22; + } + if (direction.y > threshold) { + c = (minY - origin.y) / direction.y; + d = (maxY - origin.y) / direction.y; + } else if (direction.y < -threshold) { + c = (maxY - origin.y) / direction.y; + d = (minY - origin.y) / direction.y; + } else { + c = -1e+22; + d = 1e+22; + } + if (c >= b || d <= a) return false; + if (c < a) { + if (d < b) b = d; + } else { + a = c; + if (d < b) b = d; + } + // Intersection of XY and Z projections + if (direction.z > threshold) { + c = (minZ - origin.z) / direction.z; + d = (maxZ - origin.z) / direction.z; + } else if (direction.z < -threshold) { + c = (maxZ - origin.z) / direction.z; + d = (minZ - origin.z) / direction.z; + } else { + c = -1e+22; + d = 1e+22; + } + if (c >= b || d <= a) return false; + return true; + } + + /** + * Duplicates an instance of BoundBox. + * @return New BoundBox instance with same set of properties. + */ + public function clone():BoundBox { + var res:BoundBox = new BoundBox(); + res.minX = minX; + res.minY = minY; + res.minZ = minZ; + res.maxX = maxX; + res.maxY = maxY; + res.maxZ = maxZ; + return res; + } + + /** + * Returns a string representation of BoundBox. + * @return A string representation of BoundBox. + */ + public function toString():String { + return "[BoundBox " + "X:[" + minX.toFixed(2) + ", " + maxX.toFixed(2) + "] Y:[" + minY.toFixed(2) + ", " + maxY.toFixed(2) + "] Z:[" + minZ.toFixed(2) + ", " + maxZ.toFixed(2) + "]]"; + } + + } +} diff --git a/src/alternativa/engine3d/core/Camera3D.as b/src/alternativa/engine3d/core/Camera3D.as new file mode 100644 index 0000000..4bfba4f --- /dev/null +++ b/src/alternativa/engine3d/core/Camera3D.as @@ -0,0 +1,1188 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + + import flash.display.Bitmap; + import flash.display.BitmapData; + import flash.display.DisplayObject; + import flash.display.Sprite; + import flash.display.Stage3D; + import flash.display.StageAlign; + import flash.display3D.Context3D; + import flash.events.Event; + import flash.geom.Point; + import flash.geom.Rectangle; + import flash.geom.Vector3D; + import flash.system.System; + import flash.text.TextField; + import flash.text.TextFieldAutoSize; + import flash.text.TextFormat; + import flash.utils.Dictionary; + import flash.utils.getDefinitionByName; + import flash.utils.getQualifiedClassName; + import flash.utils.getQualifiedSuperclassName; + import flash.utils.getTimer; + + use namespace alternativa3d; + +/** + * + * Camera - it's three-dimensional object without its own visual representation and intended for visualising hierarchy of objects. + * For resource optimization camera draws only visible objects(objects in frustum). The view frustum is the volume that contains + * everything that is potentially visible on the screen. This volume takes the shape of a truncated pyramid, which defines + * by 6 planes. The apex of the pyramid is the camera position and the base of the pyramid is the farClipping. + * The pyramid is truncated at the nearClipping. Current version of Alternativa3D uses Z-Buffer for sorting objects, + * accuracy of sorting depends on distance between farClipping and nearClipping. That's why necessary to set a minimum + * distance between them for current scene. nearClipping mustn't be equal zero. + * + */ +public class Camera3D extends Object3D { + + /** + * The viewport defines part of screen to which renders image seen by the camera. + * If viewport is not defined, the camera would not draws anything. + */ + public var view:View; + + /** + * Field if view. Defines in radians. Default value is Math.PI/2 which considered with 90 degrees. + */ + public var fov:Number = Math.PI / 2; + + /** + * Near clipping distance. Default value 0. It should be as big as possible. + */ + public var nearClipping:Number; + + /** + * Far distance of clipping. Default value Number.MAX_VALUE. + */ + public var farClipping:Number; + + /** + * Determines whether orthographic (true) or perspective (false) projection is used. The default value is false. + */ + public var orthographic:Boolean = false; + + /** + * @private + */ + alternativa3d var focalLength:Number; + /** + * @private + */ + alternativa3d var m0:Number; + /** + * @private + */ + alternativa3d var m5:Number; + /** + * @private + */ + alternativa3d var m10:Number; + /** + * @private + */ + alternativa3d var m14:Number; + /** + * @private + */ + alternativa3d var correctionX:Number; + /** + * @private + */ + alternativa3d var correctionY:Number; + + /** + * @private + */ + alternativa3d var lights:Vector. = new Vector.(); + /** + * @private + */ + alternativa3d var lightsLength:int = 0; + /** + * @private + */ + alternativa3d var ambient:Vector. = new Vector.(4); + /** + * @private + */ + alternativa3d var childLights:Vector. = new Vector.(); + + /** + * @private + */ + alternativa3d var frustum:CullingPlane; + + /** + * @private + */ + alternativa3d var origins:Vector. = new Vector.(); + /** + * @private + */ + alternativa3d var directions:Vector. = new Vector.(); + /** + * @private + */ + alternativa3d var raysLength:int = 0; + + /** + * @private + */ + alternativa3d var occluders:Vector. = new Vector.(); + /** + * @private + */ + alternativa3d var occludersLength:int = 0; + + /** + * @private + * Context3D which is used for rendering. + */ + alternativa3d var context3D:Context3D; + + /** + * @private + * Camera's renderer. If is not defined, the camera will no draw anything. + */ + public var renderer:Renderer = new Renderer(); + + /** + * @private + */ + alternativa3d var numDraws:int; + + /** + * @private + */ + alternativa3d var numTriangles:int; + + /** + * Creates a Camera3D object. + * + * @param nearClipping Near clipping distance. + * @param farClipping Far clipping distance. + */ + public function Camera3D(nearClipping:Number, farClipping:Number) { + this.nearClipping = nearClipping; + this.farClipping = farClipping; + frustum = new CullingPlane(); + frustum.next = new CullingPlane(); + frustum.next.next = new CullingPlane(); + frustum.next.next.next = new CullingPlane(); + frustum.next.next.next.next = new CullingPlane(); + frustum.next.next.next.next.next = new CullingPlane(); + } + + /** + * Rendering of objects hierarchy to the given Stage3D. + * + * @param stage3D Stage3D to which image will be rendered. + */ + public function render(stage3D:Stage3D):void { + var i:int; + var j:int; + var light:Light3D; + var occluder:Occluder; + // Error checking + if (stage3D == null) throw new TypeError("Parameter stage3D must be non-null."); + if (cpuTimer == -1) cpuTimer = getTimer(); + // Reset the counters + numDraws = 0; + numTriangles = 0; + // Reset the occluders + occludersLength = 0; + // Reset the lights + lightsLength = 0; + ambient[0] = 0; + ambient[1] = 0; + ambient[2] = 0; + ambient[3] = 1; + // Receiving the context + context3D = stage3D.context3D; + if (context3D != null && view != null && renderer != null && (view.stage != null || view._canvas != null)) { + renderer.camera = this; + // Projection argument calculating + calculateProjection(view._width, view._height); + // Preparing to rendering + view.prepareToRender(stage3D, context3D); + // Transformations calculating + if (transformChanged) composeTransforms(); + localToGlobalTransform.copy(transform); + globalToLocalTransform.copy(inverseTransform); + var root:Object3D = this; + while (root.parent != null) { + root = root.parent; + if (root.transformChanged) root.composeTransforms(); + localToGlobalTransform.append(root.transform); + globalToLocalTransform.prepend(root.inverseTransform); + } + // Calculating the rays of mouse events + view.calculateRays(this); + for (i = origins.length; i < view.raysLength; i++) { + origins[i] = new Vector3D(); + directions[i] = new Vector3D(); + } + raysLength = view.raysLength; + // Check if object of hierarchy is visible + if (root.visible) { + // Calculating the matrix to transform from the camera space to local space + root.cameraToLocalTransform.combine(root.inverseTransform, localToGlobalTransform); + // Calculating the matrix to transform from local space to the camera space + root.localToCameraTransform.combine(globalToLocalTransform, root.transform); + // Checking the culling + if (root.boundBox != null) { + calculateFrustum(root.cameraToLocalTransform); + root.culling = root.boundBox.checkFrustumCulling(frustum, 63); + } else { + root.culling = 63; + } + // Calculations of conent visibility + if (root.culling >= 0) root.calculateVisibility(this); + // Calculations visibility of children + root.calculateChildrenVisibility(this); + // Calculations of transformations from occluder space to the camera space + for (i = 0; i < occludersLength; i++) { + occluder = occluders[i]; + occluder.localToCameraTransform.calculateInversion(occluder.cameraToLocalTransform); + occluder.transformVertices(correctionX, correctionY); + occluder.distance = orthographic ? occluder.localToCameraTransform.l : (occluder.localToCameraTransform.d * occluder.localToCameraTransform.d + occluder.localToCameraTransform.h * occluder.localToCameraTransform.h + occluder.localToCameraTransform.l * occluder.localToCameraTransform.l); + occluder.enabled = true; + } + // Sorting the occluders by disance + if (occludersLength > 1) sortOccluders(); + // Constructing the volumes of occluders, their intersections, starts from closest + for (i = 0; i < occludersLength; i++) { + occluder = occluders[i]; + if (occluder.enabled) { + occluder.calculatePlanes(this); + if (occluder.planeList != null) { + for (j = i + 1; j < occludersLength; j++) { // It is possible, that start value should be 0 + var compared:Occluder = occluders[j]; + if (compared.enabled && compared != occluder && compared.checkOcclusion(occluder, correctionX, correctionY)) compared.enabled = false; + } + } else { + occluder.enabled = false; + } + } + // Reset of culling + occluder.culling = -1; + } + // Gather the occluders which will affects now + for (i = 0, j = 0; i < occludersLength; i++) { + occluder = occluders[i]; + if (occluder.enabled) { + // Debug + occluder.collectDraws(this, null, 0); + if (debug && occluder.boundBox != null && (checkInDebug(occluder) & Debug.BOUNDS)) Debug.drawBoundBox(this, occluder.boundBox, occluder.localToCameraTransform); + occluders[j] = occluder; + j++; + } + } + occludersLength = j; + occluders.length = j; + // Check light influence (?) + for (i = 0, j = 0; i < lightsLength; i++) { + light = lights[i]; + light.localToCameraTransform.calculateInversion(light.cameraToLocalTransform); + if (light.boundBox == null || occludersLength == 0 || !light.boundBox.checkOcclusion(occluders, occludersLength, light.localToCameraTransform)) { + light.red = ((light.color >> 16) & 0xFF) * light.intensity / 255; + light.green = ((light.color >> 8) & 0xFF) * light.intensity / 255; + light.blue = (light.color & 0xFF) * light.intensity / 255; + // Debug + light.collectDraws(this, null, 0); + if (debug && light.boundBox != null && (checkInDebug(light) & Debug.BOUNDS)) Debug.drawBoundBox(this, light.boundBox, light.localToCameraTransform); + + // Shadows preparing + if (light.shadow != null) { + light.shadow._light = light; + light.shadow.process(this); + } + lights[j] = light; + j++; + } + light.culling = -1; + } + lightsLength = j; + lights.length = j; + // Check getting in frustum and occluding + if (root.culling >= 0 && (root.boundBox == null || occludersLength == 0 || !root.boundBox.checkOcclusion(occluders, occludersLength, root.localToCameraTransform))) { + // Check if the ray crossing the bounding box + if (root.boundBox != null) { + calculateRays(root.cameraToLocalTransform); + root.listening = root.boundBox.checkRays(origins, directions, raysLength); + } else { + root.listening = true; + } + // Check if object needs in lightning + if (lightsLength > 0 && root.useLights) { + // Pass the lights to children and calculate appropriate transformations + if (root.boundBox != null) { + var childLightsLength:int = 0; + for (i = 0; i < lightsLength; i++) { + light = lights[i]; + light.lightToObjectTransform.combine(root.cameraToLocalTransform, light.localToCameraTransform); + // Detect influence + if (light.boundBox == null || light.checkBound(root)) { + childLights[childLightsLength] = light; + childLightsLength++; + } + } + root.collectDraws(this, childLights, childLightsLength); + } else { + // Calculate transformation from light space to object space + for (i = 0; i < lightsLength; i++) { + light = lights[i]; + light.lightToObjectTransform.combine(root.cameraToLocalTransform, light.localToCameraTransform); + } + root.collectDraws(this, lights, lightsLength); + } + } else { + root.collectDraws(this, null, 0); + } + // Debug the boundbox + if (debug && root.boundBox != null && (checkInDebug(root) & Debug.BOUNDS)) Debug.drawBoundBox(this, root.boundBox, root.localToCameraTransform); + } + // Gather the draws for children + root.collectChildrenDraws(this, lights, lightsLength); + } + cpuTimeSum += getTimer() - cpuTimer; + cpuTimeCount++; + // Mouse events prosessing + view.processMouseEvents(context3D, this); + // Render + renderer.render(context3D); + // Output + if (view._canvas == null) { + context3D.present(); + } else { + context3D.drawToBitmapData(view._canvas); + context3D.present(); + } + } else { + cpuTimeSum += getTimer() - cpuTimer; + cpuTimeCount++; + } + // Clearing + lights.length = 0; + childLights.length = 0; + occluders.length = 0; + context3D = null; + cpuTimer = -1; + } + + /** + * Transforms point from global space to screen space. The view property should be defined. + * @param point Point in global space. + * @return A Vector3D object containing screen coordinates. + */ + public function projectGlobal(point:Vector3D):Vector3D { + if (view == null) throw new Error("It is necessary to have view set."); + var viewSizeX:Number = view._width * 0.5; + var viewSizeY:Number = view._height * 0.5; + var focalLength:Number = Math.sqrt(viewSizeX * viewSizeX + viewSizeY * viewSizeY) / Math.tan(fov * 0.5); + var res:Vector3D = globalToLocal(point); + res.x = res.x * focalLength / res.z + viewSizeX; + res.y = res.y * focalLength / res.z + viewSizeY; + return res; + } + + /** + * Calculates a ray in global space. The ray defines by its origin and direction. + * The ray goes like from the global camera position + * trough the point corresponding to the viewport point with coordinates viewX и viewY. + * The ray origin placed within nearClipping plane. + * This ray can be used in the Object3D.intersectRay() method. The result writes to passed arguments. + * + * @param origin Ray origin will wrote here. + * @param direction Ray direction will wrote here. + * @param viewX Horizontal coordinate in view plane, through which the ray should go. + * @param viewY Vertical coordinate in view plane, through which the ray should go. + */ + public function calculateRay(origin:Vector3D, direction:Vector3D, viewX:Number, viewY:Number):void { + if (view == null) throw new Error("It is necessary to have view set."); + var viewSizeX:Number = view._width * 0.5; + var viewSizeY:Number = view._height * 0.5; + var focalLength:Number = Math.sqrt(viewSizeX * viewSizeX + viewSizeY * viewSizeY) / Math.tan(fov * 0.5); + var dx:Number = viewX - viewSizeX; + var dy:Number = viewY - viewSizeY; + var ox:Number = dx * nearClipping / focalLength; + var oy:Number = dy * nearClipping / focalLength; + var oz:Number = nearClipping; + if (transformChanged) composeTransforms(); + trm.copy(transform); + var root:Object3D = this; + while (root.parent != null) { + root = root.parent; + if (root.transformChanged) root.composeTransforms(); + trm.append(root.transform); + } + origin.x = trm.a * ox + trm.b * oy + trm.c * oz + trm.d; + origin.y = trm.e * ox + trm.f * oy + trm.g * oz + trm.h; + origin.z = trm.i * ox + trm.j * oy + trm.k * oz + trm.l; + direction.x = trm.a * dx + trm.b * dy + trm.c * focalLength; + direction.y = trm.e * dx + trm.f * dy + trm.g * focalLength; + direction.z = trm.i * dx + trm.j * dy + trm.k * focalLength; + var directionL:Number = 1 / Math.sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z); + direction.x *= directionL; + direction.y *= directionL; + direction.z *= directionL; + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:Camera3D = new Camera3D(nearClipping, farClipping); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + var src:Camera3D = source as Camera3D; + fov = src.fov; + view = src.view; + nearClipping = src.nearClipping; + farClipping = src.farClipping; + orthographic = src.orthographic; + } + + /** + * @private + */ + alternativa3d function calculateProjection(width:Number, height:Number):void { + var viewSizeX:Number = width * 0.5; + var viewSizeY:Number = height * 0.5; + focalLength = Math.sqrt(viewSizeX * viewSizeX + viewSizeY * viewSizeY) / Math.tan(fov * 0.5); + if (!orthographic) { + m0 = focalLength / viewSizeX; + m5 = -focalLength / viewSizeY; + m10 = farClipping / (farClipping - nearClipping); + m14 = -nearClipping * m10; + } else { + m0 = 1 / viewSizeX; + m5 = -1 / viewSizeY; + m10 = 1 / (farClipping - nearClipping); + m14 = -nearClipping * m10; + } + correctionX = viewSizeX / focalLength; + correctionY = viewSizeY / focalLength; + } + + /** + * @private + */ + alternativa3d function calculateFrustum(transform:Transform3D):void { + var nearPlane:CullingPlane = frustum; + var farPlane:CullingPlane = nearPlane.next; + var leftPlane:CullingPlane = farPlane.next; + var rightPlane:CullingPlane = leftPlane.next; + var topPlane:CullingPlane = rightPlane.next; + var bottomPlane:CullingPlane = topPlane.next; + if (!orthographic) { + var fa:Number = transform.a * correctionX; + var fe:Number = transform.e * correctionX; + var fi:Number = transform.i * correctionX; + var fb:Number = transform.b * correctionY; + var ff:Number = transform.f * correctionY; + var fj:Number = transform.j * correctionY; + nearPlane.x = fj * fe - ff * fi; + nearPlane.y = fb * fi - fj * fa; + nearPlane.z = ff * fa - fb * fe; + nearPlane.offset = (transform.d + transform.c * nearClipping) * nearPlane.x + (transform.h + transform.g * nearClipping) * nearPlane.y + (transform.l + transform.k * nearClipping) * nearPlane.z; + + farPlane.x = -nearPlane.x; + farPlane.y = -nearPlane.y; + farPlane.z = -nearPlane.z; + farPlane.offset = (transform.d + transform.c * farClipping) * farPlane.x + (transform.h + transform.g * farClipping) * farPlane.y + (transform.l + transform.k * farClipping) * farPlane.z; + + var ax:Number = -fa - fb + transform.c; + var ay:Number = -fe - ff + transform.g; + var az:Number = -fi - fj + transform.k; + var bx:Number = fa - fb + transform.c; + var by:Number = fe - ff + transform.g; + var bz:Number = fi - fj + transform.k; + topPlane.x = bz * ay - by * az; + topPlane.y = bx * az - bz * ax; + topPlane.z = by * ax - bx * ay; + topPlane.offset = transform.d * topPlane.x + transform.h * topPlane.y + transform.l * topPlane.z; + // Right plane. + ax = bx; + ay = by; + az = bz; + bx = fa + fb + transform.c; + by = fe + ff + transform.g; + bz = fi + fj + transform.k; + rightPlane.x = bz * ay - by * az; + rightPlane.y = bx * az - bz * ax; + rightPlane.z = by * ax - bx * ay; + rightPlane.offset = transform.d * rightPlane.x + transform.h * rightPlane.y + transform.l * rightPlane.z; + // Bottom plane. + ax = bx; + ay = by; + az = bz; + bx = -fa + fb + transform.c; + by = -fe + ff + transform.g; + bz = -fi + fj + transform.k; + bottomPlane.x = bz*ay - by*az; + bottomPlane.y = bx*az - bz*ax; + bottomPlane.z = by*ax - bx*ay; + bottomPlane.offset = transform.d*bottomPlane.x + transform.h*bottomPlane.y + transform.l*bottomPlane.z; + // Left plane. + ax = bx; + ay = by; + az = bz; + bx = -fa - fb + transform.c; + by = -fe - ff + transform.g; + bz = -fi - fj + transform.k; + leftPlane.x = bz*ay - by*az; + leftPlane.y = bx*az - bz*ax; + leftPlane.z = by*ax - bx*ay; + leftPlane.offset = transform.d*leftPlane.x + transform.h*leftPlane.y + transform.l*leftPlane.z; + } else { + var viewSizeX:Number = view._width*0.5; + var viewSizeY:Number = view._height*0.5; + // Near plane. + nearPlane.x = transform.j*transform.e - transform.f*transform.i; + nearPlane.y = transform.b*transform.i - transform.j*transform.a; + nearPlane.z = transform.f*transform.a - transform.b*transform.e; + nearPlane.offset = (transform.d + transform.c*nearClipping)*nearPlane.x + (transform.h + transform.g*nearClipping)*nearPlane.y + (transform.l + transform.k*nearClipping)*nearPlane.z; + // Far plane. + farPlane.x = -nearPlane.x; + farPlane.y = -nearPlane.y; + farPlane.z = -nearPlane.z; + farPlane.offset = (transform.d + transform.c*farClipping)*farPlane.x + (transform.h + transform.g*farClipping)*farPlane.y + (transform.l + transform.k*farClipping)*farPlane.z; + // Top plane. + topPlane.x = transform.i*transform.g - transform.e*transform.k; + topPlane.y = transform.a*transform.k - transform.i*transform.c; + topPlane.z = transform.e*transform.c - transform.a*transform.g; + topPlane.offset = (transform.d - transform.b*viewSizeY)*topPlane.x + (transform.h - transform.f*viewSizeY)*topPlane.y + (transform.l - transform.j*viewSizeY)*topPlane.z; + // Bottom plane. + bottomPlane.x = -topPlane.x; + bottomPlane.y = -topPlane.y; + bottomPlane.z = -topPlane.z; + bottomPlane.offset = (transform.d + transform.b*viewSizeY)*bottomPlane.x + (transform.h + transform.f*viewSizeY)*bottomPlane.y + (transform.l + transform.j*viewSizeY)*bottomPlane.z; + // Left plane. + leftPlane.x = transform.k*transform.f - transform.g*transform.j; + leftPlane.y = transform.c*transform.j - transform.k*transform.b; + leftPlane.z = transform.g*transform.b - transform.c*transform.f; + leftPlane.offset = (transform.d - transform.a*viewSizeX)*leftPlane.x + (transform.h - transform.e*viewSizeX)*leftPlane.y + (transform.l - transform.i*viewSizeX)*leftPlane.z; + // Right plane. + rightPlane.x = -leftPlane.x; + rightPlane.y = -leftPlane.y; + rightPlane.z = -leftPlane.z; + rightPlane.offset = (transform.d + transform.a*viewSizeX)*rightPlane.x + (transform.h + transform.e*viewSizeX)*rightPlane.y + (transform.l + transform.i*viewSizeX)*rightPlane.z; + } + } + + /** + * @private + */ + alternativa3d function calculateRays(transform:Transform3D):void { + for (var i:int = 0; i < raysLength; i++) { + var o:Vector3D = view.raysOrigins[i]; + var d:Vector3D = view.raysDirections[i]; + var origin:Vector3D = origins[i]; + var direction:Vector3D = directions[i]; + origin.x = transform.a * o.x + transform.b * o.y + transform.c * o.z + transform.d; + origin.y = transform.e * o.x + transform.f * o.y + transform.g * o.z + transform.h; + origin.z = transform.i * o.x + transform.j * o.y + transform.k * o.z + transform.l; + direction.x = transform.a * d.x + transform.b * d.y + transform.c * d.z; + direction.y = transform.e * d.x + transform.f * d.y + transform.g * d.z; + direction.z = transform.i * d.x + transform.j * d.y + transform.k * d.z; + } + } + + static private const stack:Vector. = new Vector.(); + + private function sortOccluders():void { + stack[0] = 0; + stack[1] = occludersLength - 1; + var index:int = 2; + while (index > 0) { + index--; + var r:int = stack[index]; + var j:int = r; + index--; + var l:int = stack[index]; + var i:int = l; + var occluder:Occluder = occluders[(r + l) >> 1]; + var median:Number = occluder.distance; + while (i <= j) { + var left:Occluder = occluders[i]; + while (left.distance < median) { + i++; + left = occluders[i]; + } + var right:Occluder = occluders[j]; + while (right.distance > median) { + j--; + right = occluders[j]; + } + if (i <= j) { + occluders[i] = right; + occluders[j] = left; + i++; + j--; + } + } + if (l < j) { + stack[index] = l; + index++; + stack[index] = j; + index++; + } + if (i < r) { + stack[index] = i; + index++; + stack[index] = r; + index++; + } + } + } + + // DEBUG + + /** + * Turns debug mode on if true and off otherwise. + * The default value is false. + * + * @see #addToDebug() + * @see #removeFromDebug() + */ + public var debug:Boolean = false; + + private var debugSet:Object = new Object(); + + /** + * Adds an object or a class to list of debug drawing. + * In case of class, all object of this type will drawn in debug mode. + * + * @param debug The component of object which will draws in debug mode. Should be Debug.BOUND for now. Check Debug for updates. + * @param objectOrClass Object3D or class extended Object3D. + * @see alternativa.engine3d.core.Debug + * @see #debug + * @see #removeFromDebug() + */ + public function addToDebug(debug:int, objectOrClass:*):void { + if (!debugSet[debug]) debugSet[debug] = new Dictionary(); + debugSet[debug][objectOrClass] = true; + } + + /** + * Removed an object or a class from list of debug drawing. + * + * @param debug The component of object which will draws in debug mode. Should be Debug.BOUND for now. Check Debug for updates. + * @param objectOrClass Object3D or class extended Object3D. + * + * @see alternativa.engine3d.core.Debug + * @see #debug + * @see #addToDebug() + */ + public function removeFromDebug(debug:int, objectOrClass:*):void { + if (debugSet[debug]) { + delete debugSet[debug][objectOrClass]; + var key:*; + for (key in debugSet[debug]) break; + if (!key) delete debugSet[debug]; + } + } + + /** + * @private + * + * Check if the object or its class is in list of debug drawing. + */ + alternativa3d function checkInDebug(object:Object3D):int { + var res:int = 0; + for (var debug:int = 1; debug <= 512; debug <<= 1) { + if (debugSet[debug]) { + if (debugSet[debug][Object3D] || debugSet[debug][object]) { + res |= debug; + } else { + var objectClass:Class = getDefinitionByName(getQualifiedClassName(object)) as Class; + while (objectClass != Object3D) { + if (debugSet[debug][objectClass]) { + res |= debug; + break; + } + objectClass = Class(getDefinitionByName(getQualifiedSuperclassName(objectClass))); + } + } + } + } + return res; + } + + + private var _diagram:Sprite = createDiagram(); + + /** + * The amount of frames which determines the period of FPS value update in diagram. + * @see #diagram + */ + public var fpsUpdatePeriod:int = 10; + + /** + * The amount of frames which determines the period of MS value update in diagram. + * @see #diagram + */ + public var timerUpdatePeriod:int = 10; + + private var fpsTextField:TextField; + private var frameTextField:TextField; + private var cpuTextField:TextField; + private var memoryTextField:TextField; + private var drawsTextField:TextField; + private var trianglesTextField:TextField; + private var timerTextField:TextField; + private var graph:Bitmap; + private var rect:Rectangle; + + private var _diagramAlign:String = "TR"; + private var _diagramHorizontalMargin:Number = 2; + private var _diagramVerticalMargin:Number = 2; + + private var fpsUpdateCounter:int; + private var previousFrameTime:int; + private var previousPeriodTime:int; + + private var maxMemory:int; + + private var timerUpdateCounter:int; + private var methodTimeSum:int; + private var methodTimeCount:int; + private var methodTimer:int; + private var cpuTimeSum:int = 0; + private var cpuTimeCount:int = 0; + private var cpuTimer:int = -1; + + public function startCPUTimer():void { + cpuTimer = getTimer(); + } + + /** + * Starts time count. startTimer()and stopTimer() are necessary to measure time for code part executing. + * The result is displayed in the field MS of the diagram. + * + * @see #diagram + * @see #stopTimer() + */ + public function startTimer():void { + methodTimer = getTimer(); + } + + /** + * Stops time count. startTimer() and stopTimer() are necessary to measure time for code part executing. + * The result is displayed in the field MS of the diagram. + * @see #diagram + * @see #startTimer() + */ + public function stopTimer():void { + methodTimeSum += getTimer() - methodTimer; + methodTimeCount++; + } + + /** + * Diagram where debug information is displayed. To display diagram, you need to add it on the screen. + * FPS is an average amount of frames per second. + * MS is an average time of executing the code part in milliseconds. This code part is measured with startTimer - stopTimer. + * MEM is an amount of memory reserved by player (in megabytes). + * DRW is an amount of draw calls in the current frame. + * PLG is an amount of visible polygons in the current frame. + * TRI is an amount of drawn triangles in the current frame. + * + * @see #fpsUpdatePeriod + * @see #timerUpdatePeriod + * @see #startTimer() + * @see #stopTimer() + */ + public function get diagram():DisplayObject { + return _diagram; + } + + /** + * Diagram alignment relatively to working space. You can use constants of StageAlign class. + * + */ + public function get diagramAlign():String { + return _diagramAlign; + } + + /** + * @private + */ + public function set diagramAlign(value:String):void { + _diagramAlign = value; + resizeDiagram(); + } + + /** + * Diagram margin from the edge of working space in horizontal axis. + */ + public function get diagramHorizontalMargin():Number { + return _diagramHorizontalMargin; + } + + /** + * @private + */ + public function set diagramHorizontalMargin(value:Number):void { + _diagramHorizontalMargin = value; + resizeDiagram(); + } + + /** + * Diagram margin from the edge of working space in vertical axis. + */ + public function get diagramVerticalMargin():Number { + return _diagramVerticalMargin; + } + + /** + * @private + */ + public function set diagramVerticalMargin(value:Number):void { + _diagramVerticalMargin = value; + resizeDiagram(); + } + + private function createDiagram():Sprite { + var diagram:Sprite = new Sprite(); + diagram.mouseEnabled = false; + diagram.mouseChildren = false; + // FPS + fpsTextField = new TextField(); + fpsTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0xCCCCCC); + fpsTextField.autoSize = TextFieldAutoSize.LEFT; + fpsTextField.text = "FPS:"; + fpsTextField.selectable = false; + fpsTextField.x = -3; + fpsTextField.y = -5; + diagram.addChild(fpsTextField); + // time of frame + frameTextField = new TextField(); + frameTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0xCCCCCC); + frameTextField.autoSize = TextFieldAutoSize.LEFT; + frameTextField.text = "TME:"; + frameTextField.selectable = false; + frameTextField.x = -3; + frameTextField.y = 4; + diagram.addChild(frameTextField); + // cpu time + cpuTextField = new TextField(); + cpuTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0xCCCCCC); + cpuTextField.autoSize = TextFieldAutoSize.LEFT; + cpuTextField.text = "CPU:"; + cpuTextField.selectable = false; + cpuTextField.x = -3; + cpuTextField.y = 13; + diagram.addChild(cpuTextField); + // time of method execution + timerTextField = new TextField(); + timerTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0x0066FF); + timerTextField.autoSize = TextFieldAutoSize.LEFT; + timerTextField.text = "MS:"; + timerTextField.selectable = false; + timerTextField.x = -3; + timerTextField.y = 22; + diagram.addChild(timerTextField); + // memory + memoryTextField = new TextField(); + memoryTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0xCCCC00); + memoryTextField.autoSize = TextFieldAutoSize.LEFT; + memoryTextField.text = "MEM:"; + memoryTextField.selectable = false; + memoryTextField.x = -3; + memoryTextField.y = 31; + diagram.addChild(memoryTextField); + // debug draws + drawsTextField = new TextField(); + drawsTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0x00CC00); + drawsTextField.autoSize = TextFieldAutoSize.LEFT; + drawsTextField.text = "DRW:"; + drawsTextField.selectable = false; + drawsTextField.x = -3; + drawsTextField.y = 40; + diagram.addChild(drawsTextField); + // triangles + trianglesTextField = new TextField(); + trianglesTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0xFF3300); // 0xFF6600, 0xFF0033 + trianglesTextField.autoSize = TextFieldAutoSize.LEFT; + trianglesTextField.text = "TRI:"; + trianglesTextField.selectable = false; + trianglesTextField.x = -3; + trianglesTextField.y = 49; + diagram.addChild(trianglesTextField); + // diagram initialization + diagram.addEventListener(Event.ADDED_TO_STAGE, function ():void { + // FPS + fpsTextField = new TextField(); + fpsTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0xCCCCCC); + fpsTextField.autoSize = TextFieldAutoSize.RIGHT; + fpsTextField.text = Number(diagram.stage.frameRate).toFixed(2); + fpsTextField.selectable = false; + fpsTextField.x = -3; + fpsTextField.y = -5; + fpsTextField.width = 85; + diagram.addChild(fpsTextField); + // Frame time + frameTextField = new TextField(); + frameTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0xCCCCCC); + frameTextField.autoSize = TextFieldAutoSize.RIGHT; + frameTextField.text = Number(1000 / diagram.stage.frameRate).toFixed(2); + frameTextField.selectable = false; + frameTextField.x = -3; + frameTextField.y = 4; + frameTextField.width = 85; + diagram.addChild(frameTextField); + // CPU time + cpuTextField = new TextField(); + cpuTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0xCCCCCC); + cpuTextField.autoSize = TextFieldAutoSize.RIGHT; + cpuTextField.text = ""; + cpuTextField.selectable = false; + cpuTextField.x = -3; + cpuTextField.y = 13; + cpuTextField.width = 85; + diagram.addChild(cpuTextField); + // Time of method performing + timerTextField = new TextField(); + timerTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0x0066FF); + timerTextField.autoSize = TextFieldAutoSize.RIGHT; + timerTextField.text = ""; + timerTextField.selectable = false; + timerTextField.x = -3; + timerTextField.y = 22; + timerTextField.width = 85; + diagram.addChild(timerTextField); + // Memory + memoryTextField = new TextField(); + memoryTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0xCCCC00); + memoryTextField.autoSize = TextFieldAutoSize.RIGHT; + memoryTextField.text = bytesToString(System.totalMemory); + memoryTextField.selectable = false; + memoryTextField.x = -3; + memoryTextField.y = 31; + memoryTextField.width = 85; + diagram.addChild(memoryTextField); + // Draw calls + drawsTextField = new TextField(); + drawsTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0x00CC00); + drawsTextField.autoSize = TextFieldAutoSize.RIGHT; + drawsTextField.text = "0"; + drawsTextField.selectable = false; + drawsTextField.x = -3; + drawsTextField.y = 40; + drawsTextField.width = 72; + diagram.addChild(drawsTextField); + // Number of triangles + trianglesTextField = new TextField(); + trianglesTextField.defaultTextFormat = new TextFormat("Tahoma", 10, 0xFF3300); + trianglesTextField.autoSize = TextFieldAutoSize.RIGHT; + trianglesTextField.text = "0"; + trianglesTextField.selectable = false; + trianglesTextField.x = -3; + trianglesTextField.y = 49; + trianglesTextField.width = 72; + diagram.addChild(trianglesTextField); + // Graph + graph = new Bitmap(new BitmapData(80, 40, true, 0x20FFFFFF)); + rect = new Rectangle(0, 0, 1, 40); + graph.x = 0; + graph.y = 63; + diagram.addChild(graph); + // Reset of parameters + previousPeriodTime = getTimer(); + previousFrameTime = previousPeriodTime; + fpsUpdateCounter = 0; + maxMemory = 0; + timerUpdateCounter = 0; + methodTimeSum = 0; + methodTimeCount = 0; + // Subscription + diagram.stage.addEventListener(Event.ENTER_FRAME, updateDiagram, false, -1000); + diagram.stage.addEventListener(Event.RESIZE, resizeDiagram, false, -1000); + resizeDiagram(); + }); + // Deinitialization of diagram + diagram.addEventListener(Event.REMOVED_FROM_STAGE, function ():void { + // Reset + diagram.removeChild(fpsTextField); + diagram.removeChild(frameTextField); + diagram.removeChild(cpuTextField); + diagram.removeChild(memoryTextField); + diagram.removeChild(drawsTextField); + diagram.removeChild(trianglesTextField); + diagram.removeChild(timerTextField); + diagram.removeChild(graph); + fpsTextField = null; + frameTextField = null; + cpuTextField = null; + memoryTextField = null; + drawsTextField = null; + trianglesTextField = null; + timerTextField = null; + graph.bitmapData.dispose(); + graph = null; + rect = null; + // Unsubscribe + diagram.stage.removeEventListener(Event.ENTER_FRAME, updateDiagram); + diagram.stage.removeEventListener(Event.RESIZE, resizeDiagram); + }); + return diagram; + } + + private function resizeDiagram(e:Event = null):void { + if (_diagram.stage != null) { + var coord:Point = _diagram.parent.globalToLocal(new Point()); + if (_diagramAlign == StageAlign.TOP_LEFT || _diagramAlign == StageAlign.LEFT || _diagramAlign == StageAlign.BOTTOM_LEFT) { + _diagram.x = Math.round(coord.x + _diagramHorizontalMargin); + } + if (_diagramAlign == StageAlign.TOP || _diagramAlign == StageAlign.BOTTOM) { + _diagram.x = Math.round(coord.x + _diagram.stage.stageWidth / 2 - graph.width / 2); + } + if (_diagramAlign == StageAlign.TOP_RIGHT || _diagramAlign == StageAlign.RIGHT || _diagramAlign == StageAlign.BOTTOM_RIGHT) { + _diagram.x = Math.round(coord.x + _diagram.stage.stageWidth - _diagramHorizontalMargin - graph.width); + } + if (_diagramAlign == StageAlign.TOP_LEFT || _diagramAlign == StageAlign.TOP || _diagramAlign == StageAlign.TOP_RIGHT) { + _diagram.y = Math.round(coord.y + _diagramVerticalMargin); + } + if (_diagramAlign == StageAlign.LEFT || _diagramAlign == StageAlign.RIGHT) { + _diagram.y = Math.round(coord.y + _diagram.stage.stageHeight / 2 - (graph.y + graph.height) / 2); + } + if (_diagramAlign == StageAlign.BOTTOM_LEFT || _diagramAlign == StageAlign.BOTTOM || _diagramAlign == StageAlign.BOTTOM_RIGHT) { + _diagram.y = Math.round(coord.y + _diagram.stage.stageHeight - _diagramVerticalMargin - graph.y - graph.height); + } + } + } + + private function updateDiagram(e:Event):void { + var value:Number; + var mod:int; + var time:int = getTimer(); + var stageFrameRate:int = _diagram.stage.frameRate; + + // FPS text + if (++fpsUpdateCounter == fpsUpdatePeriod) { + value = 1000 * fpsUpdatePeriod / (time - previousPeriodTime); + if (value > stageFrameRate) value = stageFrameRate; + mod = value * 100 % 100; + fpsTextField.text = int(value) + "." + ((mod >= 10) ? mod : ((mod > 0) ? ("0" + mod) : "00")); + value = 1000 / value; + mod = value * 100 % 100; + frameTextField.text = int(value) + "." + ((mod >= 10) ? mod : ((mod > 0) ? ("0" + mod) : "00")); + previousPeriodTime = time; + fpsUpdateCounter = 0; + } + // FPS plot + value = 1000 / (time - previousFrameTime); + if (value > stageFrameRate) value = stageFrameRate; + graph.bitmapData.scroll(1, 0); + graph.bitmapData.fillRect(rect, 0x20FFFFFF); + graph.bitmapData.setPixel32(0, 40 * (1 - value / stageFrameRate), 0xFFCCCCCC); + previousFrameTime = time; + + // time text + if (++timerUpdateCounter == timerUpdatePeriod) { + if (methodTimeCount > 0) { + value = methodTimeSum / methodTimeCount; + mod = value * 100 % 100; + timerTextField.text = int(value) + "." + ((mod >= 10) ? mod : ((mod > 0) ? ("0" + mod) : "00")); + } else { + timerTextField.text = ""; + } + if (cpuTimeCount > 0) { + value = cpuTimeSum / cpuTimeCount; + mod = value * 100 % 100; + cpuTextField.text = int(value) + "." + ((mod >= 10) ? mod : ((mod > 0) ? ("0" + mod) : "00")); + } else { + cpuTextField.text = ""; + } + timerUpdateCounter = 0; + methodTimeSum = 0; + methodTimeCount = 0; + cpuTimeSum = 0; + cpuTimeCount = 0; + } + + // memory text + var memory:int = System.totalMemory; + value = memory / 1048576; + mod = value * 100 % 100; + memoryTextField.text = int(value) + "." + ((mod >= 10) ? mod : ((mod > 0) ? ("0" + mod) : "00")); + + // memory plot + if (memory > maxMemory) maxMemory = memory; + graph.bitmapData.setPixel32(0, 40 * (1 - memory / maxMemory), 0xFFCCCC00); + + // debug text + drawsTextField.text = formatInt(numDraws); + + // Triangles (text) + trianglesTextField.text = formatInt(numTriangles); + } + + private function formatInt(num:int):String { + var n:int; + var s:String; + if (num < 1000) { + return "" + num; + } else if (num < 1000000) { + n = num % 1000; + if (n < 10) { + s = "00" + n; + } else if (n < 100) { + s = "0" + n; + } else { + s = "" + n; + } + return int(num / 1000) + " " + s; + } else { + n = (num % 1000000) / 1000; + if (n < 10) { + s = "00" + n; + } else if (n < 100) { + s = "0" + n; + } else { + s = "" + n; + } + n = num % 1000; + if (n < 10) { + s += " 00" + n; + } else if (n < 100) { + s += " 0" + n; + } else { + s += " " + n; + } + return int(num / 1000000) + " " + s; + } + } + + private function bytesToString(bytes:int):String { + if (bytes < 1024) return bytes + "b"; + else if (bytes < 10240) return (bytes / 1024).toFixed(2) + "kb"; + else if (bytes < 102400) return (bytes / 1024).toFixed(1) + "kb"; + else if (bytes < 1048576) return (bytes >> 10) + "kb"; + else if (bytes < 10485760) return (bytes / 1048576).toFixed(2);// + "mb"; + else if (bytes < 104857600) return (bytes / 1048576).toFixed(1);// + "mb"; + else return String(bytes >> 20);// + "mb"; + } +} +} diff --git a/src/alternativa/engine3d/core/CullingPlane.as b/src/alternativa/engine3d/core/CullingPlane.as new file mode 100644 index 0000000..52c01d7 --- /dev/null +++ b/src/alternativa/engine3d/core/CullingPlane.as @@ -0,0 +1,50 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + /** + * @private + */ + public class CullingPlane { + + public var x:Number; + public var y:Number; + public var z:Number; + public var offset:Number; + + public var next:CullingPlane; + + static public var collector:CullingPlane; + + static public function create():CullingPlane { + if (collector != null) { + var res:CullingPlane = collector; + collector = res.next; + res.next = null; + return res; + } else { + return new CullingPlane(); + } + } + + public function create():CullingPlane { + if (collector != null) { + var res:CullingPlane = collector; + collector = res.next; + res.next = null; + return res; + } else { + return new CullingPlane(); + } + } + + } +} diff --git a/src/alternativa/engine3d/core/Debug.as b/src/alternativa/engine3d/core/Debug.as new file mode 100644 index 0000000..8198cbc --- /dev/null +++ b/src/alternativa/engine3d/core/Debug.as @@ -0,0 +1,311 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.objects.WireFrame; + + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * Class stores values, that are passed to camera methods addToDebug() and removeFromDebug(). + * + * @see alternativa.engine3d.core.Camera3D#addToDebug() + * @see alternativa.engine3d.core.Camera3D#removeFromDebug() + */ + public class Debug { + + //static public const NAMES:int = 1; + + //static public const AXES:int = 2; + + //static public const CENTERS:int = 4; + + /** + * Display of objects bound boxes. + */ + static public const BOUNDS:int = 8; + + /** + * Display of content, that is depended on object type: wireframe for Mesh, schematic display for light sources. + * Now has been implemented the support of not all classes + */ + static public const CONTENT:int = 16; + + //static public const VERTICES:int = 32; + + //static public const NORMALS:int = 64; + +// /** +// * Display of object NODES, that contains tree structure. +// */ +// static public const NODES:int = 128; + +// /** +// * Display of light sources. +// */ +// static public const LIGHTS:int = 256; + +// /** +// * Display of objects joints, that contains skeletal hierarchy. +// */ +// static public const BONES:int = 512; + + static private var boundWires:Dictionary = new Dictionary(); + + static private function createBoundWire():WireFrame { + var res:WireFrame = new WireFrame(); + res.geometry.addLine(-0.5,-0.5,-0.5, 0.5,-0.5,-0.5); + res.geometry.addLine(0.5,-0.5,-0.5, 0.5,0.5,-0.5); + res.geometry.addLine(0.5,0.5,-0.5, -0.5,0.5,-0.5); + res.geometry.addLine(-0.5,0.5,-0.5, -0.5,-0.5,-0.5); + + res.geometry.addLine(-0.5,-0.5,0.5, 0.5,-0.5,0.5); + res.geometry.addLine(0.5,-0.5,0.5, 0.5,0.5,0.5); + res.geometry.addLine(0.5,0.5,0.5, -0.5,0.5,0.5); + res.geometry.addLine(-0.5,0.5,0.5, -0.5,-0.5,0.5); + + res.geometry.addLine(-0.5,-0.5,-0.5, -0.5,-0.5,0.5); + res.geometry.addLine(0.5,-0.5,-0.5, 0.5,-0.5,0.5); + res.geometry.addLine(0.5,0.5,-0.5, 0.5,0.5,0.5); + res.geometry.addLine(-0.5,0.5,-0.5, -0.5,0.5,0.5); + return res; + } + + /** + * @private + */ + static alternativa3d function drawBoundBox(camera:Camera3D, boundBox:BoundBox, transform:Transform3D, color:int = -1):void { + var boundWire:WireFrame = boundWires[camera.context3D]; + if (boundWire == null) { + boundWire = createBoundWire(); + boundWires[camera.context3D] = boundWire; + boundWire.geometry.upload(camera.context3D); + } + boundWire.color = color >= 0 ? color : 0x99FF00; + boundWire.thickness = 1; + + boundWire.transform.compose((boundBox.minX + boundBox.maxX)*0.5, (boundBox.minY + boundBox.maxY)*0.5, (boundBox.minZ + boundBox.maxZ)*0.5, 0, 0, 0, boundBox.maxX - boundBox.minX, boundBox.maxY - boundBox.minY, boundBox.maxZ - boundBox.minZ); + boundWire.localToCameraTransform.combine(transform, boundWire.transform); + boundWire.collectDraws(camera, null, 0); + } + + + /** + * @private + */ + /*static alternativa3d function drawEdges(camera:Camera3D, canvas:Canvas, list:Face, color:int):void { + var viewSizeX:Number = camera.viewSizeX; + var viewSizeY:Number = camera.viewSizeY; + var t:Number; + canvas.gfx.lineStyle(0, color); + for (var face:Face = list; face != null; face = face.processNext) { + var wrapper:Wrapper = face.wrapper; + var vertex:Vertex = wrapper.vertex; + t = 1/vertex.cameraZ; + var x:Number = vertex.cameraX*viewSizeX*t; + var y:Number = vertex.cameraY*viewSizeY*t; + canvas.gfx.moveTo(x, y); + for (wrapper = wrapper.next; wrapper != null; wrapper = wrapper.next) { + vertex = wrapper.vertex; + t = 1/vertex.cameraZ; + canvas.gfx.lineTo(vertex.cameraX*viewSizeX*t, vertex.cameraY*viewSizeY*t); + } + canvas.gfx.lineTo(x, y); + } + }*/ + + //static private const boundVertexList:Vertex = Vertex.createList(8); + + /** + * @private + */ + /*static alternativa3d function drawBounds(camera:Camera3D, canvas:Canvas, transformation:Object3D, boundMinX:Number, boundMinY:Number, boundMinZ:Number, boundMaxX:Number, boundMaxY:Number, boundMaxZ:Number, color:int = -1, alpha:Number = 1):void { + var vertex:Vertex; + // Fill + var a:Vertex = boundVertexList; + a.x = boundMinX; + a.y = boundMinY; + a.z = boundMinZ; + var b:Vertex = a.next; + b.x = boundMaxX; + b.y = boundMinY; + b.z = boundMinZ; + var c:Vertex = b.next; + c.x = boundMinX; + c.y = boundMaxY; + c.z = boundMinZ; + var d:Vertex = c.next; + d.x = boundMaxX; + d.y = boundMaxY; + d.z = boundMinZ; + var e:Vertex = d.next; + e.x = boundMinX; + e.y = boundMinY; + e.z = boundMaxZ; + var f:Vertex = e.next; + f.x = boundMaxX; + f.y = boundMinY; + f.z = boundMaxZ; + var g:Vertex = f.next; + g.x = boundMinX; + g.y = boundMaxY; + g.z = boundMaxZ; + var h:Vertex = g.next; + h.x = boundMaxX; + h.y = boundMaxY; + h.z = boundMaxZ; + // Transformation to camera + for (vertex = a; vertex != null; vertex = vertex.next) { + vertex.cameraX = transformation.ma*vertex.x + transformation.mb*vertex.y + transformation.mc*vertex.z + transformation.md; + vertex.cameraY = transformation.me*vertex.x + transformation.mf*vertex.y + transformation.mg*vertex.z + transformation.mh; + vertex.cameraZ = transformation.mi*vertex.x + transformation.mj*vertex.y + transformation.mk*vertex.z + transformation.ml; + if (vertex.cameraZ <= 0) return; + } + // Projection + var viewSizeX:Number = camera.viewSizeX; + var viewSizeY:Number = camera.viewSizeY; + for (vertex = a; vertex != null; vertex = vertex.next) { + var t:Number = 1/vertex.cameraZ; + vertex.cameraX = vertex.cameraX*viewSizeX*t; + vertex.cameraY = vertex.cameraY*viewSizeY*t; + } + // Rendering + canvas.gfx.lineStyle(0, (color < 0) ? ((transformation.culling > 0) ? 0xFFFF00 : 0x00FF00) : color, alpha); + canvas.gfx.moveTo(a.cameraX, a.cameraY); + canvas.gfx.lineTo(b.cameraX, b.cameraY); + canvas.gfx.lineTo(d.cameraX, d.cameraY); + canvas.gfx.lineTo(c.cameraX, c.cameraY); + canvas.gfx.lineTo(a.cameraX, a.cameraY); + canvas.gfx.moveTo(e.cameraX, e.cameraY); + canvas.gfx.lineTo(f.cameraX, f.cameraY); + canvas.gfx.lineTo(h.cameraX, h.cameraY); + canvas.gfx.lineTo(g.cameraX, g.cameraY); + canvas.gfx.lineTo(e.cameraX, e.cameraY); + canvas.gfx.moveTo(a.cameraX, a.cameraY); + canvas.gfx.lineTo(e.cameraX, e.cameraY); + canvas.gfx.moveTo(b.cameraX, b.cameraY); + canvas.gfx.lineTo(f.cameraX, f.cameraY); + canvas.gfx.moveTo(d.cameraX, d.cameraY); + canvas.gfx.lineTo(h.cameraX, h.cameraY); + canvas.gfx.moveTo(c.cameraX, c.cameraY); + canvas.gfx.lineTo(g.cameraX, g.cameraY); + }*/ + + //static private const nodeVertexList:Vertex = Vertex.createList(4); + + /** + * @private + */ + /*static alternativa3d function drawKDNode(camera:Camera3D, canvas:Canvas, transformation:Object3D, axis:int, coord:Number, boundMinX:Number, boundMinY:Number, boundMinZ:Number, boundMaxX:Number, boundMaxY:Number, boundMaxZ:Number, alpha:Number):void { + var vertex:Vertex; + // Fill + var a:Vertex = nodeVertexList; + var b:Vertex = a.next; + var c:Vertex = b.next; + var d:Vertex = c.next; + if (axis == 0) { + a.x = coord; + a.y = boundMinY; + a.z = boundMaxZ; + b.x = coord; + b.y = boundMaxY; + b.z = boundMaxZ; + c.x = coord; + c.y = boundMaxY; + c.z = boundMinZ; + d.x = coord; + d.y = boundMinY; + d.z = boundMinZ; + } else if (axis == 1) { + a.x = boundMaxX; + a.y = coord; + a.z = boundMaxZ; + b.x = boundMinX; + b.y = coord; + b.z = boundMaxZ; + c.x = boundMinX; + c.y = coord; + c.z = boundMinZ; + d.x = boundMaxX; + d.y = coord; + d.z = boundMinZ; + } else { + a.x = boundMinX; + a.y = boundMinY; + a.z = coord; + b.x = boundMaxX; + b.y = boundMinY; + b.z = coord; + c.x = boundMaxX; + c.y = boundMaxY; + c.z = coord; + d.x = boundMinX; + d.y = boundMaxY; + d.z = coord; + } + // Transformation to camera + for (vertex = a; vertex != null; vertex = vertex.next) { + vertex.cameraX = transformation.ma*vertex.x + transformation.mb*vertex.y + transformation.mc*vertex.z + transformation.md; + vertex.cameraY = transformation.me*vertex.x + transformation.mf*vertex.y + transformation.mg*vertex.z + transformation.mh; + vertex.cameraZ = transformation.mi*vertex.x + transformation.mj*vertex.y + transformation.mk*vertex.z + transformation.ml; + if (vertex.cameraZ <= 0) return; + } + // Projection + var viewSizeX:Number = camera.viewSizeX; + var viewSizeY:Number = camera.viewSizeY; + for (vertex = a; vertex != null; vertex = vertex.next) { + var t:Number = 1/vertex.cameraZ; + vertex.cameraX = vertex.cameraX*viewSizeX*t; + vertex.cameraY = vertex.cameraY*viewSizeY*t; + } + // Rendering + canvas.gfx.lineStyle(0, (axis == 0) ? 0xFF0000 : ((axis == 1) ? 0x00FF00 : 0x0000FF), alpha); + canvas.gfx.moveTo(a.cameraX, a.cameraY); + canvas.gfx.lineTo(b.cameraX, b.cameraY); + canvas.gfx.lineTo(c.cameraX, c.cameraY); + canvas.gfx.lineTo(d.cameraX, d.cameraY); + canvas.gfx.lineTo(a.cameraX, a.cameraY); + }*/ + + /** + * @private + */ + /*static alternativa3d function drawBone(canvas:Canvas, x1:Number, y1:Number, x2:Number, y2:Number, size:Number, color:int):void { + var nx:Number = x2 - x1; + var ny:Number = y2 - y1; + var nl:Number = Math.sqrt(nx*nx + ny*ny); + if (nl > 0.001) { + nx /= nl; + ny /= nl; + var lx:Number = ny*size; + var ly:Number = -nx*size; + var rx:Number = -ny*size; + var ry:Number = nx*size; + if (nl > size*2) { + nl = size; + } else { + nl = nl/2; + } + canvas.gfx.lineStyle(1, color); + canvas.gfx.beginFill(color, 0.6); + canvas.gfx.moveTo(x1, y1); + canvas.gfx.lineTo(x1 + nx*nl + lx, y1 + ny*nl + ly); + canvas.gfx.lineTo(x2, y2); + canvas.gfx.lineTo(x1 + nx*nl + rx, y1 + ny*nl + ry); + canvas.gfx.lineTo(x1, y1); + } + }*/ + + } +} diff --git a/src/alternativa/engine3d/core/DebugDrawUnit.as b/src/alternativa/engine3d/core/DebugDrawUnit.as new file mode 100644 index 0000000..b09de4e --- /dev/null +++ b/src/alternativa/engine3d/core/DebugDrawUnit.as @@ -0,0 +1,150 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.materials.ShaderProgram; + import alternativa.engine3d.materials.compiler.Variable; + import alternativa.engine3d.materials.compiler.VariableType; + + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * @private + */ + public class DebugDrawUnit extends DrawUnit { + + alternativa3d var shader:ShaderProgram; + + alternativa3d var vertexConstantsIndexes:Dictionary = new Dictionary(false); + alternativa3d var fragmentConstantsIndexes:Dictionary = new Dictionary(false); + + override alternativa3d function clear():void { + var k:*; + for (k in vertexConstantsIndexes) { + delete vertexConstantsIndexes[k]; + } + for (k in fragmentConstantsIndexes) { + delete fragmentConstantsIndexes[k]; + } + super.clear(); + } + + override alternativa3d function setVertexConstantsFromVector(firstRegister:int, data:Vector., numRegisters:int):void { + super.setVertexConstantsFromVector(firstRegister, data, numRegisters); + for (var i:int = 0; i < numRegisters; i++) { + vertexConstantsIndexes[int(firstRegister + i)] = true; + } + } + + override alternativa3d function setVertexConstantsFromNumbers(firstRegister:int, x:Number, y:Number, z:Number, w:Number = 1):void { + super.setVertexConstantsFromNumbers(firstRegister, x, y, z, w); + vertexConstantsIndexes[firstRegister] = true; + } + + override alternativa3d function setVertexConstantsFromTransform(firstRegister:int, transform:Transform3D):void { + super.setVertexConstantsFromTransform(firstRegister, transform); + vertexConstantsIndexes[firstRegister] = true; + vertexConstantsIndexes[int(firstRegister + 1)] = true; + vertexConstantsIndexes[int(firstRegister + 2)] = true; + } + + alternativa3d override function setProjectionConstants(camera:Camera3D, firstRegister:int, transform:Transform3D = null):void { + super.setProjectionConstants(camera, firstRegister, transform); + vertexConstantsIndexes[firstRegister] = true; + vertexConstantsIndexes[int(firstRegister + 1)] = true; + vertexConstantsIndexes[int(firstRegister + 2)] = true; + vertexConstantsIndexes[int(firstRegister + 3)] = true; + } + + override alternativa3d function setFragmentConstantsFromVector(firstRegister:int, data:Vector., numRegisters:int):void { + super.setFragmentConstantsFromVector(firstRegister, data, numRegisters); + for (var i:int = 0; i < numRegisters; i++) { + fragmentConstantsIndexes[int(firstRegister + i)] = true; + } + } + + override alternativa3d function setFragmentConstantsFromNumbers(firstRegister:int, x:Number, y:Number, z:Number, w:Number = 1):void { + super.setFragmentConstantsFromNumbers(firstRegister, x, y, z, w); + fragmentConstantsIndexes[firstRegister] = true; + } + + override alternativa3d function setFragmentConstantsFromTransform(firstRegister:int, transform:Transform3D):void { + super.setFragmentConstantsFromTransform(firstRegister, transform); + fragmentConstantsIndexes[firstRegister] = true; + fragmentConstantsIndexes[int(firstRegister + 1)] = true; + fragmentConstantsIndexes[int(firstRegister + 2)] = true; + } + + public function check():void { + if (object == null) throw new Error("Object not set."); + if (program == null) throw new Error("Program not set."); + if (indexBuffer == null) throw new Error("IndexBuffer not set."); + + if (shader == null) return; + var index:int; + var variable:Variable; + for each (variable in shader.vertexShader._linkedVariables) { + index = variable.index; + if (index >= 0) { + switch (variable.type) { + case VariableType.ATTRIBUTE: + if (!hasVertexBuffer(index)) { + throw new Error("VertexBuffer " + index + " with variable name " + variable.name + " not set."); + } + break; + case VariableType.CONSTANT: + if (!vertexConstantsIndexes[index]) { + throw new Error("Vertex Constant " + index + " with variable name " + variable.name + " not set."); + } + break; + } + } + } + for each (variable in shader.fragmentShader._linkedVariables) { + index = variable.index; + if (index >= 0) { + switch (variable.type) { + case VariableType.SAMPLER: + if (!hasTexture(index)) { + throw new Error("Sampler " + index + " with variable name " + variable.name + " not set."); + } + break; + case VariableType.CONSTANT: + if (!fragmentConstantsIndexes[index]) { + throw new Error("Fragment Constant " + index + " with variable name " + variable.name + " not set."); + } + break; + } + } + } + } + + private function hasVertexBuffer(index:int):Boolean { + for (var i:int = 0; i < vertexBuffersLength; i++) { + if (vertexBuffersIndexes[i] == index) { + return true; + } + } + return false; + } + private function hasTexture(index:int):Boolean { + for (var i:int = 0; i < texturesLength; i++) { + if (texturesSamplers[i] == index) { + return true; + } + } + return false; + } + + } +} diff --git a/src/alternativa/engine3d/core/DebugMaterialsRenderer.as b/src/alternativa/engine3d/core/DebugMaterialsRenderer.as new file mode 100644 index 0000000..0238a27 --- /dev/null +++ b/src/alternativa/engine3d/core/DebugMaterialsRenderer.as @@ -0,0 +1,50 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.materials.ShaderProgram; + + import flash.display3D.IndexBuffer3D; + import flash.display3D.Program3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class DebugMaterialsRenderer extends Renderer { + + override alternativa3d function createDrawUnit(object:Object3D, program:Program3D, indexBuffer:IndexBuffer3D, firstIndex:int, numTriangles:int, debugShader:ShaderProgram = null):DrawUnit { + var res:DebugDrawUnit; + if (collector != null) { + res = DebugDrawUnit(collector); + collector = collector.next; + res.next = null; + } else { + res = new DebugDrawUnit(); + } + res.shader = debugShader; + res.object = object; + res.program = program; + res.indexBuffer = indexBuffer; + res.firstIndex = firstIndex; + res.numTriangles = numTriangles; + return res; + } + + override alternativa3d function addDrawUnit(drawUnit:DrawUnit, renderPriority:int):void { + DebugDrawUnit(drawUnit).check(); + super.addDrawUnit(drawUnit, renderPriority); + } + + } +} diff --git a/src/alternativa/engine3d/core/DrawUnit.as b/src/alternativa/engine3d/core/DrawUnit.as new file mode 100644 index 0000000..45a029d --- /dev/null +++ b/src/alternativa/engine3d/core/DrawUnit.as @@ -0,0 +1,248 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DTriangleFace; + import flash.display3D.IndexBuffer3D; + import flash.display3D.Program3D; + import flash.display3D.VertexBuffer3D; + import flash.display3D.textures.TextureBase; + + use namespace alternativa3d; + + /** + * @private + */ + public class DrawUnit { + + alternativa3d var next:DrawUnit; + + // Required parameters + alternativa3d var object:Object3D; + alternativa3d var program:Program3D; + alternativa3d var indexBuffer:IndexBuffer3D; + alternativa3d var firstIndex:int; + alternativa3d var numTriangles:int; + + // Additional parameters + alternativa3d var blendSource:String = Context3DBlendFactor.ONE; + alternativa3d var blendDestination:String = Context3DBlendFactor.ZERO; + alternativa3d var culling:String = Context3DTriangleFace.FRONT; + + // Textures + alternativa3d var textures:Vector. = new Vector.(); + alternativa3d var texturesSamplers:Vector. = new Vector.(); + alternativa3d var texturesLength:int = 0; + + // Vertex buffers + alternativa3d var vertexBuffers:Vector. = new Vector.(); + alternativa3d var vertexBuffersIndexes:Vector. = new Vector.(); + alternativa3d var vertexBuffersOffsets:Vector. = new Vector.(); + alternativa3d var vertexBuffersFormats:Vector. = new Vector.(); + alternativa3d var vertexBuffersLength:int = 0; + + // Constants + alternativa3d var vertexConstants:Vector. = new Vector.(); + alternativa3d var vertexConstantsRegistersCount:int = 0; + alternativa3d var fragmentConstants:Vector. = new Vector.(28*4, true); + alternativa3d var fragmentConstantsRegistersCount:int = 0; + + public function DrawUnit() { + } + + alternativa3d function clear():void { + object = null; + program = null; + indexBuffer = null; + blendSource = Context3DBlendFactor.ONE; + blendDestination = Context3DBlendFactor.ZERO; + culling = Context3DTriangleFace.FRONT; + textures.length = 0; + texturesLength = 0; + vertexBuffers.length = 0; + vertexBuffersLength = 0; + vertexConstantsRegistersCount = 0; + fragmentConstantsRegistersCount = 0; + } + + alternativa3d function setTextureAt(sampler:int, texture:TextureBase):void { + if (uint(sampler) > 8) throw new Error("Sampler index " + sampler + " is out of bounds."); + if (texture == null) throw new Error("Texture is null"); + texturesSamplers[texturesLength] = sampler; + textures[texturesLength] = texture; + texturesLength++; + } + + alternativa3d function setVertexBufferAt(index:int, buffer:VertexBuffer3D, bufferOffset:int, format:String):void { + if (uint(index) > 8) throw new Error("VertexBuffer index " + index + " is out of bounds."); + if (buffer == null) throw new Error("Buffer is null"); + vertexBuffersIndexes[vertexBuffersLength] = index; + vertexBuffers[vertexBuffersLength] = buffer; + vertexBuffersOffsets[vertexBuffersLength] = bufferOffset; + vertexBuffersFormats[vertexBuffersLength] = format; + vertexBuffersLength++; + } + + alternativa3d function setVertexConstantsFromVector(firstRegister:int, data:Vector., numRegisters:int):void { + if (uint(firstRegister) > (128 - numRegisters)) throw new Error("Register index " + firstRegister + " is out of bounds."); + var offset:int = firstRegister << 2; + if (firstRegister + numRegisters > vertexConstantsRegistersCount) { + vertexConstantsRegistersCount = firstRegister + numRegisters; + vertexConstants.length = vertexConstantsRegistersCount << 2; + } + for (var i:int = 0, len:int = numRegisters << 2; i < len; i++) { + vertexConstants[offset] = data[i]; + offset++; + } + } + + alternativa3d function setVertexConstantsFromNumbers(firstRegister:int, x:Number, y:Number, z:Number, w:Number = 1):void { + if (uint(firstRegister) > 127) throw new Error("Register index " + firstRegister + " is out of bounds."); + var offset:int = firstRegister << 2; + if (firstRegister + 1 > vertexConstantsRegistersCount) { + vertexConstantsRegistersCount = firstRegister + 1; + vertexConstants.length = vertexConstantsRegistersCount << 2; + } + vertexConstants[offset] = x; offset++; + vertexConstants[offset] = y; offset++; + vertexConstants[offset] = z; offset++; + vertexConstants[offset] = w; + } + + alternativa3d function setVertexConstantsFromTransform(firstRegister:int, transform:Transform3D):void { + if (uint(firstRegister) > 125) throw new Error("Register index " + firstRegister + " is out of bounds."); + var offset:int = firstRegister << 2; + if (firstRegister + 3 > vertexConstantsRegistersCount) { + vertexConstantsRegistersCount = firstRegister + 3; + vertexConstants.length = vertexConstantsRegistersCount << 2; + } + vertexConstants[offset] = transform.a; offset++; + vertexConstants[offset] = transform.b; offset++; + vertexConstants[offset] = transform.c; offset++; + vertexConstants[offset] = transform.d; offset++; + vertexConstants[offset] = transform.e; offset++; + vertexConstants[offset] = transform.f; offset++; + vertexConstants[offset] = transform.g; offset++; + vertexConstants[offset] = transform.h; offset++; + vertexConstants[offset] = transform.i; offset++; + vertexConstants[offset] = transform.j; offset++; + vertexConstants[offset] = transform.k; offset++; + vertexConstants[offset] = transform.l; + } + + /** + * @private + */ + alternativa3d function setProjectionConstants(camera:Camera3D, firstRegister:int, transform:Transform3D = null):void { + if (uint(firstRegister) > 124) throw new Error("Register index is out of bounds."); + var offset:int = firstRegister << 2; + if (firstRegister + 4 > vertexConstantsRegistersCount) { + vertexConstantsRegistersCount = firstRegister + 4; + vertexConstants.length = vertexConstantsRegistersCount << 2; + } + if (transform != null) { + vertexConstants[offset] = transform.a*camera.m0; offset++; + vertexConstants[offset] = transform.b*camera.m0; offset++; + vertexConstants[offset] = transform.c*camera.m0; offset++; + vertexConstants[offset] = transform.d*camera.m0; offset++; + vertexConstants[offset] = transform.e*camera.m5; offset++; + vertexConstants[offset] = transform.f*camera.m5; offset++; + vertexConstants[offset] = transform.g*camera.m5; offset++; + vertexConstants[offset] = transform.h*camera.m5; offset++; + vertexConstants[offset] = transform.i*camera.m10; offset++; + vertexConstants[offset] = transform.j*camera.m10; offset++; + vertexConstants[offset] = transform.k*camera.m10; offset++; + vertexConstants[offset] = transform.l*camera.m10 + camera.m14; offset++; + if (!camera.orthographic) { + vertexConstants[offset] = transform.i; offset++; + vertexConstants[offset] = transform.j; offset++; + vertexConstants[offset] = transform.k; offset++; + vertexConstants[offset] = transform.l; + } else { + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 1; + } + } else { + vertexConstants[offset] = camera.m0; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = camera.m5; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = camera.m10; offset++; + vertexConstants[offset] = camera.m14; offset++; + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 0; offset++; + if (!camera.orthographic) { + vertexConstants[offset] = 1; offset++; + vertexConstants[offset] = 0; + } else { + vertexConstants[offset] = 0; offset++; + vertexConstants[offset] = 1; + } + } + } + + alternativa3d function setFragmentConstantsFromVector(firstRegister:int, data:Vector., numRegisters:int):void { + if (uint(firstRegister) > (28 - numRegisters)) throw new Error("Register index " + firstRegister + " is out of bounds."); + var offset:int = firstRegister << 2; + if (firstRegister + numRegisters > fragmentConstantsRegistersCount) { + fragmentConstantsRegistersCount = firstRegister + numRegisters; + } + for (var i:int = 0, len:int = numRegisters << 2; i < len; i++) { + fragmentConstants[offset] = data[i]; + offset++; + } + } + + alternativa3d function setFragmentConstantsFromNumbers(firstRegister:int, x:Number, y:Number, z:Number, w:Number = 1):void { + if (uint(firstRegister) > 27) throw new Error("Register index " + firstRegister + " is out of bounds."); + var offset:int = firstRegister << 2; + if (firstRegister + 1 > fragmentConstantsRegistersCount) { + fragmentConstantsRegistersCount = firstRegister + 1; + } + fragmentConstants[offset] = x; offset++; + fragmentConstants[offset] = y; offset++; + fragmentConstants[offset] = z; offset++; + fragmentConstants[offset] = w; + } + + alternativa3d function setFragmentConstantsFromTransform(firstRegister:int, transform:Transform3D):void { + if (uint(firstRegister) > 25) throw new Error("Register index " + firstRegister + " is out of bounds."); + var offset:int = firstRegister << 2; + if (firstRegister + 3 > fragmentConstantsRegistersCount) { + fragmentConstantsRegistersCount = firstRegister + 3; + } + fragmentConstants[offset] = transform.a; offset++; + fragmentConstants[offset] = transform.b; offset++; + fragmentConstants[offset] = transform.c; offset++; + fragmentConstants[offset] = transform.d; offset++; + fragmentConstants[offset] = transform.e; offset++; + fragmentConstants[offset] = transform.f; offset++; + fragmentConstants[offset] = transform.g; offset++; + fragmentConstants[offset] = transform.h; offset++; + fragmentConstants[offset] = transform.i; offset++; + fragmentConstants[offset] = transform.j; offset++; + fragmentConstants[offset] = transform.k; offset++; + fragmentConstants[offset] = transform.l; + } + + } +} diff --git a/src/alternativa/engine3d/core/Light3D.as b/src/alternativa/engine3d/core/Light3D.as new file mode 100644 index 0000000..162802a --- /dev/null +++ b/src/alternativa/engine3d/core/Light3D.as @@ -0,0 +1,116 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.shadows.Shadow; + + use namespace alternativa3d; + + /** + * Base class for light sources. Light sources are involved in the hierarchy of 3d objects, + * have transformation and bounding boxes (BoundBox). + * Light source influences on objects, boundboxes of which intersect with boundbox of the given light source. + * + * Light3D does not meant for instantiating, use subclasses instead. + * + * @see alternativa.engine3d.core.BoundBox + */ + public class Light3D extends Object3D { + + public var shadow:Shadow; + + /** + * Color of the light. + */ + public var color:uint; + + /** + * Intensity. + */ + public var intensity:Number = 1; + + /** + * @private + */ + alternativa3d var lightToObjectTransform:Transform3D = new Transform3D(); + + /** + * @private + */ + alternativa3d var lightID:String; + /** + * @private + */ + alternativa3d var red:Number; + /** + * @private + */ + alternativa3d var green:Number; + /** + * @private + */ + alternativa3d var blue:Number; + + /** + * @private + */ + private static var lastLightNumber:uint = 0; + /** + * @private + */ + public function Light3D() { + lightID = "l" + lastLightNumber.toString(16); + name = "L" + (lastLightNumber++).toString(); + } + + /** + * @private + */ + override alternativa3d function calculateVisibility(camera:Camera3D):void { + if (intensity != 0 && color > 0) { + camera.lights[camera.lightsLength] = this; + camera.lightsLength++; + } + } + + /** + * @private + * Check if given object placed in field of influence of the light. + * @param targetObject Object for checking. + * @return True + */ + alternativa3d function checkBound(targetObject:Object3D):Boolean { + // this check is implemented in subclasses + return true; + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:Light3D = new Light3D(); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + var src:Light3D = source as Light3D; + color = src.color; + intensity = src.intensity; + } + + } +} diff --git a/src/alternativa/engine3d/core/Object3D.as b/src/alternativa/engine3d/core/Object3D.as new file mode 100644 index 0000000..925eade --- /dev/null +++ b/src/alternativa/engine3d/core/Object3D.as @@ -0,0 +1,1453 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.collisions.EllipsoidCollider; + import alternativa.engine3d.core.events.Event3D; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.objects.Surface; + + import flash.events.Event; + import flash.events.EventPhase; + import flash.events.IEventDispatcher; + import flash.geom.Matrix3D; + import flash.geom.Vector3D; + import flash.utils.Dictionary; + import flash.utils.getQualifiedClassName; + + use namespace alternativa3d; + + /** + * Dispatches when an Object3D is added as a child to another Object3D. + * Following methods generate this event: Object3D.addChild(), Object3D.addChildAt(). + * + * @see #addChild() + * @see #addChildAt() + * + * @eventType alternativa.engine3d.core.events.Event3D.ADDED + */ + [Event(name="added",type="alternativa.engine3d.core.events.Event3D")] + + /** + * Dispatched when a Object3D is about to be removed from the children list. + * Following methods generate this event: Object3D.removeChild() and Object3D.removeChildAt(). + * + * @see #removeChild() + * @see #removeChildAt() + * @eventType alternativa.engine3d.core.events.Event3D.REMOVED + */ + [Event(name="removed",type="alternativa.engine3d.core.events.Event3D")] + + /** + * Dispatched when a user presses and releases the main button + * of the user's pointing device over the same Object3D. + * Any other evens can occur between pressing and releasing the button. + * + * @eventType alternativa.engine3d.events.MouseEvent3D.CLICK + */ + [Event (name="click", type="alternativa.engine3d.core.events.MouseEvent3D")] + + /** + * Dispatched when a user presses and releases the main button of + * a pointing device twice in rapid succession over the same Object3D. + * + * @eventType alternativa.engine3d.events.MouseEvent3D.DOUBLE_CLICK + */ + [Event (name="doubleClick", type="alternativa.engine3d.core.events.MouseEvent3D")] + + /** + * Dispatched when a user presses the pointing device button over an Object3D instance. + * @eventType alternativa.engine3d.events.MouseEvent3D.MOUSE_DOWN + */ + [Event (name="mouseDown", type="alternativa.engine3d.core.events.MouseEvent3D")] + + /** + * Dispatched when a user releases the pointing device button over an Object3D instance. + * @eventType alternativa.engine3d.events.MouseEvent3D.MOUSE_UP + */ + [Event (name="mouseUp", type="alternativa.engine3d.core.events.MouseEvent3D")] + + /** + * Dispatched when the user moves a pointing device over an Object3D instance. + * @eventType alternativa.engine3d.events.MouseEvent3D.MOUSE_OVER + */ + [Event (name="mouseOver", type="alternativa.engine3d.core.events.MouseEvent3D")] + + /** + * Dispatched when the user moves a pointing device away from an Object3D instance. + * @eventType alternativa.engine3d.events.MouseEvent3D.MOUSE_OUT + */ + [Event (name="mouseOut", type="alternativa.engine3d.core.events.MouseEvent3D")] + + /** + * Dispatched when the user moves a pointing device over an Object3D instance. + * @eventType alternativa.engine3d.events.MouseEvent3D.ROLL_OVER + */ + [Event (name="rollOver", type="alternativa.engine3d.core.events.MouseEvent3D")] + + /** + * Dispatched when the user moves a pointing device away from an Object3D instance. + * @eventType alternativa.engine3d.events.MouseEvent3D.ROLL_OUT + */ + [Event (name="rollOut", type="alternativa.engine3d.core.events.MouseEvent3D")] + + /** + * Dispatched when a user moves the pointing device while it is over an Object3D. + * @eventType alternativa.engine3d.events.MouseEvent3D.MOUSE_MOVE + */ + [Event (name="mouseMove", type="alternativa.engine3d.core.events.MouseEvent3D")] + + /** + * Dispatched when a mouse wheel is spun over an Object3D instance. + * @eventType alternativa.engine3d.events.MouseEvent3D.MOUSE_WHEEL + */ + [Event (name="mouseWheel", type="alternativa.engine3d.core.events.MouseEvent3D")] + + /** + * Object3D class ia a base class for all 3D objects. Any Object3D has a property + * of transformation that defines its position in space, the property boundBox, + * which describes the rectangular parallelepiped into which fits this 3D object. + * The last feature of this class is the one place in the 3d hierarchy like + * DisplayObject has its own place in Display List. + * Unlike the previous version Alternativa3D, an instance of this class can contain many children, + * so it can act as a container. This also applies to all the inheritors Object3D . + * + * @see alternativa.engine3d.objects.Mesh + * @see alternativa.engine3d.core.BoundBox + */ + public class Object3D implements IEventDispatcher { + + /** + * Custom data available to store within Object3D by user. + */ + public var userData:Object; + + /** + * @private + */ + public var useShadow:Boolean = true; + + /** + * @private + */ + alternativa3d static const trm:Transform3D = new Transform3D(); + + /** + * Name of the object. + */ + public var name:String; + + /** + * Whether or not the display object is visible. + */ + public var visible:Boolean = true; + + /** + * Specifies whether this object receives mouse, or other user input, messages. + * The default value is true. + * + * The behaviour is consistent with behaviour of flash.display.InteractiveObject. + * + */ + public var mouseEnabled:Boolean = true; + + /** + * Determines whether or not the children of the object are mouse, or user input device, enabled. + * In case of false, the value of target property of the event + * will be the self Object3D wether mouse pointed on it or on its child. + * The default value is true. + */ + public var mouseChildren:Boolean = true; + + /** + * Specifies whether the object receives doubleClick events. + * The default value is false, which means that by default an Object3D + * instance does not receive doubleClick events. + * + * The doubleClickEnabled property of current stage also should be true. + */ + public var doubleClickEnabled:Boolean = false; + + /** + * A Boolean value that indicates whether the pointing hand (hand cursor) + * appears when the pointer rolls over a Object3D. + */ + public var useHandCursor:Boolean = false; + + /** + * Bounds of the object described as rectangular parallelepiped. + */ + public var boundBox:BoundBox; + + /** + * @private + */ + alternativa3d var _x:Number = 0; + + /** + * @private + */ + alternativa3d var _y:Number = 0; + + /** + * @private + */ + alternativa3d var _z:Number = 0; + + /** + * @private + */ + alternativa3d var _rotationX:Number = 0; + + /** + * @private + */ + alternativa3d var _rotationY:Number = 0; + + /** + * @private + */ + alternativa3d var _rotationZ:Number = 0; + + /** + * @private + */ + alternativa3d var _scaleX:Number = 1; + + /** + * @private + */ + alternativa3d var _scaleY:Number = 1; + + /** + * @private + */ + alternativa3d var _scaleZ:Number = 1; + + /** + * @private + */ + alternativa3d var _parent:Object3D; + + /** + * @private + */ + alternativa3d var childrenList:Object3D; + + /** + * @private + */ + alternativa3d var next:Object3D; + + /** + * @private + */ + alternativa3d var transform:Transform3D = new Transform3D(); + + /** + * @private + */ + alternativa3d var inverseTransform:Transform3D = new Transform3D(); + + /** + * @private + */ + alternativa3d var transformChanged:Boolean = true; + + /** + * @private + */ + alternativa3d var cameraToLocalTransform:Transform3D = new Transform3D(); + + /** + * @private + */ + alternativa3d var localToCameraTransform:Transform3D = new Transform3D(); + + /** + * @private + */ + alternativa3d var localToGlobalTransform:Transform3D = new Transform3D(); + + /** + * @private + */ + alternativa3d var globalToLocalTransform:Transform3D = new Transform3D(); + + /** + * @private + */ + alternativa3d var culling:int; + + /** + * @private + */ + alternativa3d var listening:Boolean; + + /** + * @private + */ + alternativa3d var distance:Number; + + /** + * @private + */ + alternativa3d var bubbleListeners:Object; + + /** + * @private + */ + alternativa3d var captureListeners:Object; + + /** + * @private + */ + alternativa3d var transformProcedure:Procedure; + + /** + * @private + */ + alternativa3d var deltaTransformProcedure:Procedure; + + /** + * X coordinate. + */ + public function get x():Number { + return _x; + } + + /** + * @private + */ + public function set x(value:Number):void { + if (_x != value) { + _x = value; + transformChanged = true; + } + } + + /** + * Y coordinate. + */ + public function get y():Number { + return _y; + } + + /** + * @private + */ + public function set y(value:Number):void { + if (_y != value) { + _y = value; + transformChanged = true; + } + } + + /** + * Z coordinate. + */ + public function get z():Number { + return _z; + } + + /** + * @private + */ + public function set z(value:Number):void { + if (_z != value) { + _z = value; + transformChanged = true; + } + } + + /** + * The angle of rotation of Object3D around the X-axis expressed in radians. + */ + public function get rotationX():Number { + return _rotationX; + } + + /** + * @private + */ + public function set rotationX(value:Number):void { + if (_rotationX != value) { + _rotationX = value; + transformChanged = true; + } + } + + /** + * The angle of rotation of Object3D around the Y-axis expressed in radians. + */ + public function get rotationY():Number { + return _rotationY; + } + + /** + * @private + */ + public function set rotationY(value:Number):void { + if (_rotationY != value) { + _rotationY = value; + transformChanged = true; + } + } + + /** + * The angle of rotation of Object3D around the Z-axis expressed in radians. + */ + public function get rotationZ():Number { + return _rotationZ; + } + + /** + * @private + */ + public function set rotationZ(value:Number):void { + if (_rotationZ != value) { + _rotationZ = value; + transformChanged = true; + } + } + + /** + * The scale of the Object3D along the X-axis. + */ + public function get scaleX():Number { + return _scaleX; + } + + /** + * @private + */ + public function set scaleX(value:Number):void { + if (_scaleX != value) { + _scaleX = value; + transformChanged = true; + } + } + + /** + * The scale of the Object3D along the Y-axis. + */ + public function get scaleY():Number { + return _scaleY; + } + + /** + * @private + */ + public function set scaleY(value:Number):void { + if (_scaleY != value) { + _scaleY = value; + transformChanged = true; + } + } + + /** + * The scale of the Object3D along the Z-axis. + */ + public function get scaleZ():Number { + return _scaleZ; + } + + /** + * @private + */ + public function set scaleZ(value:Number):void { + if (_scaleZ != value) { + _scaleZ = value; + transformChanged = true; + } + } + + /** + * The matrix property represents a transformation matrix that determines the position + * and orientation of an Object3D. + */ + public function get matrix():Matrix3D { + if (transformChanged) composeTransforms(); + return new Matrix3D(Vector.([transform.a, transform.e, transform.i, 0, transform.b, transform.f, transform.j, 0, transform.c, transform.g, transform.k, 0, transform.d, transform.h, transform.l, 1])); + } + + /** + * @private + */ + public function set matrix(value:Matrix3D):void { + var v:Vector. = value.decompose(); + var t:Vector3D = v[0]; + var r:Vector3D = v[1]; + var s:Vector3D = v[2]; + _x = t.x; + _y = t.y; + _z = t.z; + _rotationX = r.x; + _rotationY = r.y; + _rotationZ = r.z; + _scaleX = s.x; + _scaleY = s.y; + _scaleZ = s.z; + transformChanged = true; + } + + /** + * Searches for the intersection of an Object3D and given ray, defined by origin and direction. + * + * @param origin Origin of the ray. + * @param direction Direction of the ray. + * @return The result of searching given as RayIntersectionData. null will returned in case of intersection was not found. + * @see RayIntersectionData + * @see alternativa.engine3d.objects.Sprite3D + * @see alternativa.engine3d.core.Camera3D#calculateRay() + */ + public function intersectRay(origin:Vector3D, direction:Vector3D):RayIntersectionData { + return intersectRayChildren(origin, direction); + } + + /** + * @private + */ + alternativa3d function intersectRayChildren(origin:Vector3D, direction:Vector3D):RayIntersectionData { + var minTime:Number = 1e22; + var minData:RayIntersectionData = null; + var childOrigin:Vector3D; + var childDirection:Vector3D; + for (var child:Object3D = childrenList; child != null; child = child.next) { + if (child.transformChanged) child.composeTransforms(); + if (childOrigin == null) { + childOrigin = new Vector3D(); + childDirection = new Vector3D(); + } + childOrigin.x = child.inverseTransform.a*origin.x + child.inverseTransform.b*origin.y + child.inverseTransform.c*origin.z + child.inverseTransform.d; + childOrigin.y = child.inverseTransform.e*origin.x + child.inverseTransform.f*origin.y + child.inverseTransform.g*origin.z + child.inverseTransform.h; + childOrigin.z = child.inverseTransform.i*origin.x + child.inverseTransform.j*origin.y + child.inverseTransform.k*origin.z + child.inverseTransform.l; + childDirection.x = child.inverseTransform.a*direction.x + child.inverseTransform.b*direction.y + child.inverseTransform.c*direction.z; + childDirection.y = child.inverseTransform.e*direction.x + child.inverseTransform.f*direction.y + child.inverseTransform.g*direction.z; + childDirection.z = child.inverseTransform.i*direction.x + child.inverseTransform.j*direction.y + child.inverseTransform.k*direction.z; + var data:RayIntersectionData = child.intersectRay(childOrigin, childDirection); + if (data != null && data.time < minTime) { + minData = data; + minTime = data.time; + } + } + return minData; + } + + /** + * A Matrix3D object representing the combined transformation matrices of the Object3D + * and all of its parent objects, back to the root level. + */ + public function get concatenatedMatrix():Matrix3D { + if (transformChanged) composeTransforms(); + trm.copy(transform); + var root:Object3D = this; + while (root.parent != null) { + root = root.parent; + if (root.transformChanged) root.composeTransforms(); + trm.append(root.transform); + } + return new Matrix3D(Vector.([trm.a, trm.e, trm.i, 0, trm.b, trm.f, trm.j, 0, trm.c, trm.g, trm.k, 0, trm.d, trm.h, trm.l, 1])); + } + + /** + * Converts the Vector3D object from the Object3D's own (local) coordinates to the root Object3D (global) coordinates. + * @param point Point in local coordinates of Object3D. + * @return Point in coordinates of root Object3D. + */ + public function localToGlobal(point:Vector3D):Vector3D { + if (transformChanged) composeTransforms(); + trm.copy(transform); + var root:Object3D = this; + while (root.parent != null) { + root = root.parent; + if (root.transformChanged) root.composeTransforms(); + trm.append(root.transform); + } + var res:Vector3D = new Vector3D(); + res.x = trm.a*point.x + trm.b*point.y + trm.c*point.z + trm.d; + res.y = trm.e*point.x + trm.f*point.y + trm.g*point.z + trm.h; + res.z = trm.i*point.x + trm.j*point.y + trm.k*point.z + trm.l; + return res; + } + + /** + * Converts the Vector3D object from the root Object3D (global) coordinates to the local Object3D's own coordinates. + * @param point Point in coordinates of root Object3D. + * @return Point in local coordinates of Object3D. + */ + public function globalToLocal(point:Vector3D):Vector3D { + if (transformChanged) composeTransforms(); + trm.copy(inverseTransform); + var root:Object3D = this; + while (root.parent != null) { + root = root.parent; + if (root.transformChanged) root.composeTransforms(); + trm.prepend(root.inverseTransform); + } + var res:Vector3D = new Vector3D(); + res.x = trm.a*point.x + trm.b*point.y + trm.c*point.z + trm.d; + res.y = trm.e*point.x + trm.f*point.y + trm.g*point.z + trm.h; + res.z = trm.i*point.x + trm.j*point.y + trm.k*point.z + trm.l; + return res; + } + + /** + * @private + */ + alternativa3d function get useLights():Boolean { + return false; + } + + /** + * Calculates object's bounds in its own coordinates + */ + public function calculateBoundBox():void { + if (boundBox != null) { + boundBox.reset(); + } else { + boundBox = new BoundBox(); + } + // Fill values of th boundBox + updateBoundBox(boundBox, null); + } + + /** + * @private + */ + alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + } + + /** + * Registers an event listener object with an EventDispatcher object + * so that the listener receives notification of an event. + * @param type The type of event. + * @param listener The listener function that processes the event. + * @param useCapture Determines whether the listener works in the capture phase or the target and bubbling phases. + * @param priority The priority level of the event listener. + * @param useWeakReference Does not used. + */ + public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void { + if (listener == null) throw new TypeError("Parameter listener must be non-null."); + var listeners:Object; + if (useCapture) { + if (captureListeners == null) captureListeners = new Object(); + listeners = captureListeners; + } else { + if (bubbleListeners == null) bubbleListeners = new Object(); + listeners = bubbleListeners; + } + var vector:Vector. = listeners[type]; + if (vector == null) { + vector = new Vector.(); + listeners[type] = vector; + } + if (vector.indexOf(listener) < 0) { + vector.push(listener); + } + } + + /** + * Removes a listener from the EventDispatcher object. + * @param type The type of event. + * @param listener The listener object to remove. + * @param useCapture Specifies whether the listener was registered for the capture phase or the target and bubbling phases. + */ + public function removeEventListener(type:String, listener:Function, useCapture:Boolean = false):void { + if (listener == null) throw new TypeError("Parameter listener must be non-null."); + var listeners:Object = useCapture ? captureListeners : bubbleListeners; + if (listeners != null) { + var vector:Vector. = listeners[type]; + if (vector != null) { + var i:int = vector.indexOf(listener); + if (i >= 0) { + var length:int = vector.length; + for (var j:int = i + 1; j < length; j++,i++) { + vector[i] = vector[j]; + } + if (length > 1) { + vector.length = length - 1; + } else { + delete listeners[type]; + var key:*; + for (key in listeners) break; + if (!key) { + if (listeners == captureListeners) { + captureListeners = null; + } else { + bubbleListeners = null; + } + } + } + } + } + } + } + + /** + * Checks whether the EventDispatcher object has any listeners registered for a specific type of event. + * @param type The type of event. + * @return A value of true if a listener of the specified type is registered; false otherwise. + */ + public function hasEventListener(type:String):Boolean { + return captureListeners != null && captureListeners[type] || bubbleListeners != null && bubbleListeners[type]; + } + + /** + * Checks whether an event listener is registered with this EventDispatcher object or any of its ancestors for the specified event type. + * @param type The type of event. + * @return A value of true if a listener of the specified type will be triggered; false otherwise. + */ + public function willTrigger(type:String):Boolean { + for (var object:Object3D = this; object != null; object = object._parent) { + if (object.captureListeners != null && object.captureListeners[type] || object.bubbleListeners != null && object.bubbleListeners[type]) return true; + } + return false; + } + + /** + * Dispatches an event into the event flow. In case of dispatched event extends Event class, properties target and currentTarget + * will not be set. They will be set if dispatched event extends Event3D oe subclasses. + * @param event The Event object that is dispatched into the event flow. + * @return A value of true if the event was successfully dispatched. Otherwise returns false. + */ + public function dispatchEvent(event:Event):Boolean { + if (event == null) throw new TypeError("Parameter event must be non-null."); + var event3D:Event3D = event as Event3D; + if (event3D != null) { + event3D._target = this; + } + var branch:Vector. = new Vector.(); + var branchLength:int = 0; + var object:Object3D; + var i:int; + var j:int; + var length:int; + var vector:Vector.; + var functions:Vector.; + for (object = this; object != null; object = object._parent) { + branch[branchLength] = object; + branchLength++; + } + // capture phase + for (i = branchLength - 1; i > 0; i--) { + object = branch[i]; + if (event3D != null) { + event3D._currentTarget = object; + event3D._eventPhase = EventPhase.CAPTURING_PHASE; + } + + if (object.captureListeners != null) { + vector = object.captureListeners[event.type]; + if (vector != null) { + length = vector.length; + functions = new Vector.(); + for (j = 0; j < length; j++) functions[j] = vector[j]; + for (j = 0; j < length; j++) (functions[j] as Function).call(null, event); + } + } + } + if (event3D != null) { + event3D._eventPhase = EventPhase.AT_TARGET; + } + // target + bubbles phases + for (i = 0; i < branchLength; i++) { + object = branch[i]; + if (event3D != null) { + event3D._currentTarget = object; + if (i > 0) { + event3D._eventPhase = EventPhase.BUBBLING_PHASE; + } + } + if (object.bubbleListeners != null) { + vector = object.bubbleListeners[event.type]; + if (vector != null) { + length = vector.length; + functions = new Vector.(); + for (j = 0; j < length; j++) functions[j] = vector[j]; + for (j = 0; j < length; j++) (functions[j] as Function).call(null, event); + } + } + if (!event.bubbles) break; + } + return true; + } + + /** + * Object3D, to which this object was added as a child. + */ + public function get parent():Object3D { + return _parent; + } + + /** + * @private + */ + alternativa3d function removeFromParent():void { + if (_parent != null) { + _parent.removeFromList(this); + _parent = null; + } + } + + /** + * Adds given Object3D instance as a child to the end of this Object3D's children list. + * If the given object was added to another Object3D already, it removes from it's old place. + * @param child The Object3D instance to add. + * @return The Object3D instance that you pass in the child parameter. + */ + public function addChild(child:Object3D):Object3D { + // Error checking + if (child == null) throw new TypeError("Parameter child must be non-null."); + if (child == this) throw new ArgumentError("An object cannot be added as a child of itself."); + for (var container:Object3D = _parent; container != null; container = container._parent) { + if (container == child) throw new ArgumentError("An object cannot be added as a child to one of it's children (or children's children, etc.)."); + } + // Adding + if (child._parent != this) { + // Removing from old place + if (child._parent != null) child._parent.removeChild(child); + // Adding + addToList(child); + child._parent = this; + // Dispatching the event + if (child.willTrigger(Event3D.ADDED)) child.dispatchEvent(new Event3D(Event3D.ADDED, true)); + } else { + child = removeFromList(child); + if (child == null) throw new ArgumentError("Cannot add child."); + // Adding + addToList(child); + } + return child; + } + + /** + * Removes the specified child Object3D instance from the child list of the + * this Object3D instance. The parent property of the removed child is set to null. + * + * @param child The Object3D instance to remove. + * @return The Object3D instance that you pass in the child parameter. + */ + public function removeChild(child:Object3D):Object3D { + // Error checking + if (child == null) throw new TypeError("Parameter child must be non-null."); + if (child._parent != this) throw new ArgumentError("The supplied Object3D must be a child of the caller."); + child = removeFromList(child); + if (child == null) throw new ArgumentError("Cannot remove child."); + // Dispatching the event + if (child.willTrigger(Event3D.REMOVED)) child.dispatchEvent(new Event3D(Event3D.REMOVED, true)); + child._parent = null; + return child; + } + + /** + * Adds a child Object3D instance to this Object3D instance. The child is added at the index position specified. + * @param child The Object3D instance to add as a child of this Object3D instance. + * @param index The index position to which the child is added. + * @return The Object3D instance that you pass in the child parameter. + */ + public function addChildAt(child:Object3D, index:int):Object3D { + // Error checking + if (child == null) throw new TypeError("Parameter child must be non-null."); + if (child == this) throw new ArgumentError("An object cannot be added as a child of itself."); + if (index < 0) throw new RangeError("The supplied index is out of bounds."); + for (var container:Object3D = _parent; container != null; container = container._parent) { + if (container == child) throw new ArgumentError("An object cannot be added as a child to one of it's children (or children's children, etc.)."); + } + // Search for element by index + var current:Object3D = childrenList; + for (var i:int = 0; i < index; i++) { + if (current == null) throw new RangeError("The supplied index is out of bounds."); + current = current.next; + } + // Adding + if (child._parent != this) { + // Removing from old parent + if (child._parent != null) child._parent.removeChild(child); + // Adding + addToList(child, current); + child._parent = this; + // Dispatching the event + if (child.willTrigger(Event3D.ADDED)) child.dispatchEvent(new Event3D(Event3D.ADDED, true)); + } else { + child = removeFromList(child); + if (child == null) throw new ArgumentError("Cannot add child."); + // Adding + addToList(child, current); + } + return child; + } + + /** + * Removes a child Object3D from the specified index position in the child list of + * the Object3D. The parent property of the removed child is set to null. + * + * @param index The child index of the Object3D to remove. + * @return The Object3D instance that was removed. + */ + public function removeChildAt(index:int):Object3D { + // Error checking + if (index < 0) throw new RangeError("The supplied index is out of bounds."); + // Search for element by index + var child:Object3D = childrenList; + for (var i:int = 0; i < index; i++) { + if (child == null) throw new RangeError("The supplied index is out of bounds."); + child = child.next; + } + if (child == null) throw new RangeError("The supplied index is out of bounds."); + // Removing + removeFromList(child); + // Dispatching the event + if (child.willTrigger(Event3D.REMOVED)) child.dispatchEvent(new Event3D(Event3D.REMOVED, true)); + child._parent = null; + return child; + } + + /** + * Removes child objects in given range of indexes. + * @param beginIndex Index, starts from which objects should be removed. + * @param endIndex Index, till which objects should be removed. + */ + public function removeChildren(beginIndex:int = 0, endIndex:int = 2147483647):void { + // Error checking + if (beginIndex < 0) throw new RangeError("The supplied index is out of bounds."); + if (endIndex < beginIndex) throw new RangeError("The supplied index is out of bounds."); + var i:int = 0; + var prev:Object3D = null; + var begin:Object3D = childrenList; + while (i < beginIndex) { + if (begin == null) { + if (endIndex < 2147483647) { + throw new RangeError("The supplied index is out of bounds."); + } else { + return; + } + } + prev = begin; + begin = begin.next; + i++; + } + if (begin == null) { + if (endIndex < 2147483647) { + throw new RangeError("The supplied index is out of bounds."); + } else { + return; + } + } + var end:Object3D = null; + if (endIndex < 2147483647) { + end = begin; + while (i <= endIndex) { + if (end == null) throw new RangeError("The supplied index is out of bounds."); + end = end.next; + i++; + } + } + if (prev != null) { + prev.next = end; + } else { + childrenList = end; + } + // Removing + while (begin != end) { + var next:Object3D = begin.next; + begin.next = null; + if (begin.willTrigger(Event3D.REMOVED)) begin.dispatchEvent(new Event3D(Event3D.REMOVED, true)); + begin._parent = null; + begin = next; + } + } + + /** + * Returns the child Object3D instance that exists at the specified index. + * @param index Position of wished child. + * @return Child object at given position. + */ + public function getChildAt(index:int):Object3D { + // Error checking + if (index < 0) throw new RangeError("The supplied index is out of bounds."); + // Search for element by index + var current:Object3D = childrenList; + for (var i:int = 0; i < index; i++) { + if (current == null) throw new RangeError("The supplied index is out of bounds."); + current = current.next; + } + if (current == null) throw new RangeError("The supplied index is out of bounds."); + return current; + } + + /** + * Returns index of given child Object3D instance. + * @param child Child Object3D instance. + * @return Index of given child Object3D instance. + */ + public function getChildIndex(child:Object3D):int { + // Error checking + if (child == null) throw new TypeError("Parameter child must be non-null."); + if (child._parent != this) throw new ArgumentError("The supplied Object3D must be a child of the caller."); + // Search for index + var index:int = 0; + for (var current:Object3D = childrenList; current != null; current = current.next) { + if (current == child) return index; + index++; + } + throw new ArgumentError("Cannot get child index."); + } + + /** + * Sets index for child Object3D instance. + * @param child Child Object3D instance. + * @param index Index should be set. + */ + public function setChildIndex(child:Object3D, index:int):void { + // Error checking + if (child == null) throw new TypeError("Parameter child must be non-null."); + if (child._parent != this) throw new ArgumentError("The supplied Object3D must be a child of the caller."); + if (index < 0) throw new RangeError("The supplied index is out of bounds."); + // Search for element by index + var current:Object3D = childrenList; + for (var i:int = 0; i < index; i++) { + if (current == null) throw new RangeError("The supplied index is out of bounds."); + current = current.next; + } + // Removing + child = removeFromList(child); + if (child == null) throw new ArgumentError("Cannot set child index."); + // Adding + addToList(child, current); + } + + /** + * Swaps index positions of two specified child objects. + * @param child1 The first object to swap. + * @param child2 The second object to swap. + */ + public function swapChildren(child1:Object3D, child2:Object3D):void { + // Error checking + if (child1 == null || child2 == null) throw new TypeError("Parameter child must be non-null."); + if (child1._parent != this || child2._parent != this) throw new ArgumentError("The supplied Object3D must be a child of the caller."); + // Swapping + if (child1 != child2) { + if (child1.next == child2) { + child2 = removeFromList(child2); + if (child2 == null) throw new ArgumentError("Cannot swap children."); + addToList(child2, child1); + } else if (child2.next == child1) { + child1 = removeFromList(child1); + if (child1 == null) throw new ArgumentError("Cannot swap children."); + addToList(child1, child2); + } else { + var count:int = 0; + for (var child:Object3D = childrenList; child != null; child = child.next) { + if (child == child1) count++; + if (child == child2) count++; + if (count == 2) break; + } + if (count < 2) throw new ArgumentError("Cannot swap children."); + var nxt:Object3D = child1.next; + removeFromList(child1); + addToList(child1, child2); + removeFromList(child2); + addToList(child2, nxt); + } + } + } + + /** + * Swaps index positions of two child objects by its index. + * @param index1 Index of the first object to swap. + * @param index2 Index of the second object to swap. + */ + public function swapChildrenAt(index1:int, index2:int):void { + // Error checking + if (index1 < 0 || index2 < 0) throw new RangeError("The supplied index is out of bounds."); + // Swapping + if (index1 != index2) { + // Search for element by index + var i:int; + var child1:Object3D = childrenList; + for (i = 0; i < index1; i++) { + if (child1 == null) throw new RangeError("The supplied index is out of bounds."); + child1 = child1.next; + } + if (child1 == null) throw new RangeError("The supplied index is out of bounds."); + var child2:Object3D = childrenList; + for (i = 0; i < index2; i++) { + if (child2 == null) throw new RangeError("The supplied index is out of bounds."); + child2 = child2.next; + } + if (child2 == null) throw new RangeError("The supplied index is out of bounds."); + if (child1 != child2) { + if (child1.next == child2) { + removeFromList(child2); + addToList(child2, child1); + } else if (child2.next == child1) { + removeFromList(child1); + addToList(child1, child2); + } else { + var nxt:Object3D = child1.next; + removeFromList(child1); + addToList(child1, child2); + removeFromList(child2); + addToList(child2, nxt); + } + } + } + } + + /** + * Returns child Object3D instance with given name. + * In case of there are several objects with same name, the first of them will returned. + * If there are no objects with given name, null will returned. + * + * @param name The name of child object. + * @return Child Object3D with given name. + */ + public function getChildByName(name:String):Object3D { + // Error checking + if (name == null) throw new TypeError("Parameter name must be non-null."); + // Search for object + for (var child:Object3D = childrenList; child != null; child = child.next) { + if (child.name == name) return child; + } + return null; + } + + /** + * Check if given object is child of this Object3D. + * @param child Child Object3D instance. + * @return true if given instance is this Object3D or one of its children or false otherwise. + */ + public function contains(child:Object3D):Boolean { + // Error checking + if (child == null) throw new TypeError("Parameter child must be non-null."); + // Search for object + if (child == this) return true; + for (var object:Object3D = childrenList; object != null; object = object.next) { + if (object.contains(child)) return true; + } + return false; + } + + /** + * Returns the number of children of this object. + */ + public function get numChildren():int { + var num:int = 0; + for (var current:Object3D = childrenList; current != null; current = current.next) num++; + return num; + } + + private function addToList(child:Object3D, item:Object3D = null):void { + child.next = item; + if (item == childrenList) { + childrenList = child; + } else { + for (var current:Object3D = childrenList; current != null; current = current.next) { + if (current.next == item) { + current.next = child; + break; + } + } + } + } + + /** + * @private + */ + alternativa3d function removeFromList(child:Object3D):Object3D { + var prev:Object3D; + for (var current:Object3D = childrenList; current != null; current = current.next) { + if (current == child) { + if (prev != null) { + prev.next = current.next; + } else { + childrenList = current.next; + } + current.next = null; + return child; + } + prev = current; + } + return null; + } + + /** + * Gather the resources of this Object3D. This resources should be uploaded in the Context3D in order to Object3D can be rendered. + * + * @param hierarchy If true, the resources of all children will be gathered too. + * @param resourceType If defined, only resources of this type will be gathered. + * @return Vector consists of gathered resources + * @see flash.display.Stage3D + */ + public function getResources(hierarchy:Boolean = false, resourceType:Class = null):Vector. { + var res:Vector. = new Vector.(); + var dict:Dictionary = new Dictionary(); + var count:int = 0; + fillResources(dict, hierarchy, resourceType); + for (var key:* in dict) { + res[count++] = key as Resource; + } + return res; + } + + /** + * @private + */ + alternativa3d function fillResources(resources:Dictionary, hierarchy:Boolean = false, resourceType:Class = null):void { + if (hierarchy) { + for (var child:Object3D = childrenList; child != null; child = child.next) { + child.fillResources(resources, hierarchy, resourceType); + } + } + } + + /** + * @private + */ + alternativa3d function composeTransforms():void { + // Matrix + var cosX:Number = Math.cos(_rotationX); + var sinX:Number = Math.sin(_rotationX); + var cosY:Number = Math.cos(_rotationY); + var sinY:Number = Math.sin(_rotationY); + var cosZ:Number = Math.cos(_rotationZ); + var sinZ:Number = Math.sin(_rotationZ); + var cosZsinY:Number = cosZ*sinY; + var sinZsinY:Number = sinZ*sinY; + var cosYscaleX:Number = cosY*_scaleX; + var sinXscaleY:Number = sinX*_scaleY; + var cosXscaleY:Number = cosX*_scaleY; + var cosXscaleZ:Number = cosX*_scaleZ; + var sinXscaleZ:Number = sinX*_scaleZ; + transform.a = cosZ*cosYscaleX; + transform.b = cosZsinY*sinXscaleY - sinZ*cosXscaleY; + transform.c = cosZsinY*cosXscaleZ + sinZ*sinXscaleZ; + transform.d = _x; + transform.e = sinZ*cosYscaleX; + transform.f = sinZsinY*sinXscaleY + cosZ*cosXscaleY; + transform.g = sinZsinY*cosXscaleZ - cosZ*sinXscaleZ; + transform.h = _y; + transform.i = -sinY*_scaleX; + transform.j = cosY*sinXscaleY; + transform.k = cosY*cosXscaleZ; + transform.l = _z; + // Inverse matrix + var sinXsinY:Number = sinX*sinY; + cosYscaleX = cosY/_scaleX; + cosXscaleY = cosX/_scaleY; + sinXscaleZ = -sinX/_scaleZ; + cosXscaleZ = cosX/_scaleZ; + inverseTransform.a = cosZ*cosYscaleX; + inverseTransform.b = sinZ*cosYscaleX; + inverseTransform.c = -sinY/_scaleX; + inverseTransform.d = -inverseTransform.a*_x - inverseTransform.b*_y - inverseTransform.c*_z; + inverseTransform.e = sinXsinY*cosZ/_scaleY - sinZ*cosXscaleY; + inverseTransform.f = cosZ*cosXscaleY + sinXsinY*sinZ/_scaleY; + inverseTransform.g = sinX*cosY/_scaleY; + inverseTransform.h = -inverseTransform.e*_x - inverseTransform.f*_y - inverseTransform.g*_z; + inverseTransform.i = cosZ*sinY*cosXscaleZ - sinZ*sinXscaleZ; + inverseTransform.j = cosZ*sinXscaleZ + sinY*sinZ*cosXscaleZ; + inverseTransform.k = cosY*cosXscaleZ; + inverseTransform.l = -inverseTransform.i*_x - inverseTransform.j*_y - inverseTransform.k*_z; + transformChanged = false; + } + + /** + * @private + */ + alternativa3d function calculateVisibility(camera:Camera3D):void { + } + + /** + * @private + */ + alternativa3d function calculateChildrenVisibility(camera:Camera3D):void { + for (var child:Object3D = childrenList; child != null; child = child.next) { + if (child.visible) { + // Compose matrix and inverse matrix + if (child.transformChanged) child.composeTransforms(); + // Calculating matrix for converting from camera coordinates to local coordinates + child.cameraToLocalTransform.combine(child.inverseTransform, cameraToLocalTransform); + // Calculating matrix for converting from local coordinates to camera coordinates + child.localToCameraTransform.combine(localToCameraTransform, child.transform); + // Culling checking + if (child.boundBox != null) { + camera.calculateFrustum(child.cameraToLocalTransform); + child.culling = child.boundBox.checkFrustumCulling(camera.frustum, 63); + } else { + child.culling = 63; + } + // Calculating visibility of the self content + if (child.culling >= 0) child.calculateVisibility(camera); + // Calculating visibility of children + if (child.childrenList != null) child.calculateChildrenVisibility(camera); + } + } + } + + /** + * @private + */ + alternativa3d function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + } + + /** + * @private + */ + alternativa3d function collectChildrenDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + var i:int; + var light:Light3D; + for (var child:Object3D = childrenList; child != null; child = child.next) { + if (child.visible) { + // Check getting in frustum and occluding + if (child.culling >= 0 && (child.boundBox == null || camera.occludersLength == 0 || !child.boundBox.checkOcclusion(camera.occluders, camera.occludersLength, child.localToCameraTransform))) { + // Check if the ray crossing the bounding box + if (child.boundBox != null) { + camera.calculateRays(child.cameraToLocalTransform); + child.listening = child.boundBox.checkRays(camera.origins, camera.directions, camera.raysLength); + } else { + child.listening = true; + } + // Check if object needs in lightning + if (lightsLength > 0 && child.useLights) { + // Pass the lights to children and calculate appropriate transformations + if (child.boundBox != null) { + var childLightsLength:int = 0; + for (i = 0; i < lightsLength; i++) { + light = lights[i]; + light.lightToObjectTransform.combine(child.cameraToLocalTransform, light.localToCameraTransform); + // Detect influence + if (light.boundBox == null || light.checkBound(child)) { + camera.childLights[childLightsLength] = light; + childLightsLength++; + } + } + child.collectDraws(camera, camera.childLights, childLightsLength); + } else { + // Calculate transformation from light space to object space + for (i = 0; i < lightsLength; i++) { + light = lights[i]; + light.lightToObjectTransform.combine(child.cameraToLocalTransform, light.localToCameraTransform); + } + child.collectDraws(camera, lights, lightsLength); + } + } else { + child.collectDraws(camera, null, 0); + } + // Debug the boundbox + if (camera.debug && child.boundBox != null && (camera.checkInDebug(child) & Debug.BOUNDS)) Debug.drawBoundBox(camera, child.boundBox, child.localToCameraTransform); + } + // Gather the draws for children + if (child.childrenList != null) child.collectChildrenDraws(camera, lights, lightsLength); + } + } + } + + /** + * @private + */ + alternativa3d function collectGeometry(collider:EllipsoidCollider, excludedObjects:Dictionary):void { + } + + /** + * @private + */ + alternativa3d function collectChildrenGeometry(collider:EllipsoidCollider, excludedObjects:Dictionary):void { + for (var child:Object3D = childrenList; child != null; child = child.next) { + if (excludedObjects == null || !excludedObjects[child]) { + // Compose matrix and inverse matrix if it needed + if (child.transformChanged) child.composeTransforms(); + // Calculating matrix for converting from collider coordinates to local coordinates + child.globalToLocalTransform.combine(child.inverseTransform, globalToLocalTransform); + // Check boundbox intersecting + var intersects:Boolean = true; + if (child.boundBox != null) { + collider.calculateSphere(child.globalToLocalTransform); + intersects = child.boundBox.checkSphere(collider.sphere); + } + // Adding the geometry of self content + if (intersects) { + // Calculating matrix for converting from local coordinates to callider coordinates + child.localToGlobalTransform.combine(localToGlobalTransform, child.transform); + child.collectGeometry(collider, excludedObjects); + } + // Check for children + if (child.childrenList != null) child.collectChildrenGeometry(collider, excludedObjects); + } + } + } + + /** + * @private + */ + alternativa3d function setTransformConstants(drawUnit:DrawUnit, surface:Surface, vertexShader:Linker, camera:Camera3D):void { + } + + + /** + * Returns a copy of this Object3D. + * @return A copy of this Object3D. + */ + public function clone():Object3D { + var res:Object3D = new Object3D(); + res.clonePropertiesFrom(this); + return res; + } + + /** + * Copies basic properties of Object3D. This method calls from clone() method. + * @param source Object3D, properties of which will be copied. + */ + protected function clonePropertiesFrom(source:Object3D):void { + userData = source.userData; + + name = source.name; + visible = source.visible; + mouseEnabled = source.mouseEnabled; + mouseChildren = source.mouseChildren; + doubleClickEnabled = source.doubleClickEnabled; + useHandCursor = source.useHandCursor; + boundBox = source.boundBox ? source.boundBox.clone() : null; + _x = source._x; + _y = source._y; + _z = source._z; + _rotationX = source._rotationX; + _rotationY = source._rotationY; + _rotationZ = source._rotationZ; + _scaleX = source._scaleX; + _scaleY = source._scaleY; + _scaleZ = source._scaleZ; + for (var child:Object3D = source.childrenList, lastChild:Object3D; child != null; child = child.next) { + var newChild:Object3D = child.clone(); + if (childrenList != null) { + lastChild.next = newChild; + } else { + childrenList = newChild; + } + lastChild = newChild; + newChild._parent = this; + } + } + + /** + * Returns the string representation of the specified object. + * @return The string representation of the specified object. + */ + public function toString():String { + var className:String = getQualifiedClassName(this); + return "[" + className.substr(className.indexOf("::") + 2) + " " + name + "]"; + } + + } +} diff --git a/src/alternativa/engine3d/core/Occluder.as b/src/alternativa/engine3d/core/Occluder.as new file mode 100644 index 0000000..335f03f --- /dev/null +++ b/src/alternativa/engine3d/core/Occluder.as @@ -0,0 +1,1386 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.objects.WireFrame; + import alternativa.engine3d.resources.Geometry; + + import flash.utils.ByteArray; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * Polygonal Object3D meant for excluding from the rendering process those objects, which it shields from the camera. + * The Occluder has no visual representation and does not be render. + * The geometry should be a convex polygon. + */ + public class Occluder extends Object3D { + + private var faceList:Face; + + private var edgeList:Edge; + + private var vertexList:Vertex; + + private var debugWire:WireFrame; + + /** + * @private + */ + alternativa3d var planeList:CullingPlane; + + /** + * @private + */ + alternativa3d var enabled:Boolean; + + /** + * Minimal ratio of overlap area of viewport by occluder to viewport area. + * This property can has value from 0 to 1. + */ + public var minSize:Number = 0; + + /** + * Creates form of overlap on base of re-created geometry. + * Geometry must be solid, closed and convex. + * @param geometry passed Geometry + * @param distanceThreshold Accuracy, within which the coordinates of the vertices are the same. + * @param weldTriangles If true, then related triangles, that lie in one plane, will be united in one polygon. + * @param angleThreshold Permissible angle in radians between normals, that allows to unite faces in one plane. + * @param convexThreshold Value, that decrease allowable angle between related edges of united faces. + * @see #destroyForm() + */ + public function createForm(geometry:Geometry, distanceThreshold:Number = 0, weldTriangles:Boolean = true, angleThreshold:Number = 0, convexThreshold:Number = 0):void { + destroyForm(); + // Checking for the errors + var geometryIndicesLength:int = geometry._indices.length; + if (geometry._numVertices == 0 || geometryIndicesLength == 0) throw new Error("The supplied geometry is empty."); + var vBuffer:VertexStream = (VertexAttributes.POSITION < geometry._attributesStreams.length) ? geometry._attributesStreams[VertexAttributes.POSITION] : null; + if (vBuffer == null) throw new Error("The supplied geometry is empty."); + var i:int; + // Create vertices + var vertices:Vector. = new Vector.(); + var attributesOffset:int = geometry._attributesOffsets[VertexAttributes.POSITION]; + var numMappings:int = vBuffer.attributes.length; + var data:ByteArray = vBuffer.data; + for (i = 0; i < geometry._numVertices; i++) { + data.position = 4*(numMappings*i + attributesOffset); + var vertex:Vertex = new Vertex(); + vertex.x = data.readFloat(); + vertex.y = data.readFloat(); + vertex.z = data.readFloat(); + vertices[i] = vertex; + } + // Create faces + for (i = 0; i < geometryIndicesLength;) { + var a:int = geometry._indices[i]; i++; + var b:int = geometry._indices[i]; i++; + var c:int = geometry._indices[i]; i++; + var face:Face = new Face(); + face.wrapper = new Wrapper(); + face.wrapper.vertex = vertices[a]; + face.wrapper.next = new Wrapper(); + face.wrapper.next.vertex = vertices[b]; + face.wrapper.next.next = new Wrapper(); + face.wrapper.next.next.vertex = vertices[c]; + face.calculateBestSequenceAndNormal(); + face.next = faceList; + faceList = face; + } + // Unite vertices + vertexList = weldVertices(vertices, distanceThreshold); + // Unite faces + if (weldTriangles) weldFaces(angleThreshold, convexThreshold); + // Calculation of edges and checking for the validity + var error:String = calculateEdges(); + if (error != null) { + destroyForm(); + throw new ArgumentError(error); + } + calculateBoundBox(); + } + + /** + * Destroys form of overlap. + * @see #createForm() + */ + public function destroyForm():void { + faceList = null; + edgeList = null; + vertexList = null; + if (debugWire != null) { + debugWire.geometry.dispose(); + debugWire = null; + } + } + + /** + * @private + */ + override alternativa3d function calculateVisibility(camera:Camera3D):void { + camera.occluders[camera.occludersLength] = this; + camera.occludersLength++; + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + // Debug + if (camera.debug) { + if (camera.checkInDebug(this) & Debug.CONTENT) { + if (debugWire == null) { + debugWire = new WireFrame(0xFF00FF, 1, 2); + for (var edge:Edge = edgeList; edge != null; edge = edge.next) { + debugWire.geometry.addLine(edge.a.x, edge.a.y, edge.a.z, edge.b.x, edge.b.y, edge.b.z); + } + debugWire.geometry.upload(camera.context3D); + } + debugWire.localToCameraTransform.copy(localToCameraTransform); + debugWire.collectDraws(camera, null, 0); + } + } + } + + private function calculateEdges():String { + var face:Face; + var wrapper:Wrapper; + var edge:Edge; + // Create edges + for (face = faceList; face != null; face = face.next) { + // Loop of edge segments + var a:Vertex; + var b:Vertex; + for (wrapper = face.wrapper; wrapper != null; wrapper = wrapper.next, a = b) { + a = wrapper.vertex; + b = (wrapper.next != null) ? wrapper.next.vertex : face.wrapper.vertex; + // Loop of created edges + for (edge = edgeList; edge != null; edge = edge.next) { + // If geometry is incorrect + if (edge.a == a && edge.b == b) { + return "The supplied geometry is not valid."; + } + // If found created edges with these vertices + if (edge.a == b && edge.b == a) break; + } + if (edge != null) { + edge.right = face; + } else { + edge = new Edge(); + edge.a = a; + edge.b = b; + edge.left = face; + edge.next = edgeList; + edgeList = edge; + } + } + } + // Checking for the validity + for (edge = edgeList; edge != null; edge = edge.next) { + // If edge consists of one face + if (edge.left == null || edge.right == null) { + return "The supplied geometry is non whole."; + } + var abx:Number = edge.b.x - edge.a.x; + var aby:Number = edge.b.y - edge.a.y; + var abz:Number = edge.b.z - edge.a.z; + var crx:Number = edge.right.normalZ*edge.left.normalY - edge.right.normalY*edge.left.normalZ; + var cry:Number = edge.right.normalX*edge.left.normalZ - edge.right.normalZ*edge.left.normalX; + var crz:Number = edge.right.normalY*edge.left.normalX - edge.right.normalX*edge.left.normalY; + // If bend inside + if (abx*crx + aby*cry + abz*crz < 0) { + //return "The supplied geometry is non convex."; + trace("Warning: " + this + ": geometry is non convex."); + } + } + return null; + } + + private function weldVertices(vertices:Vector., distanceThreshold:Number):Vertex { + var vertex:Vertex; + var verticesLength:int = vertices.length; + // Group + group(vertices, 0, verticesLength, 0, distanceThreshold, new Vector.()); + // Change vertices + for (var face:Face = faceList; face != null; face = face.next) { + for (var wrapper:Wrapper = face.wrapper; wrapper != null; wrapper = wrapper.next) { + if (wrapper.vertex.value != null) { + wrapper.vertex = wrapper.vertex.value; + } + } + } + // Create new list of vertices + var res:Vertex; + for (var i:int = 0; i < verticesLength; i++) { + vertex = vertices[i]; + if (vertex.value == null) { + vertex.next = res; + res = vertex; + } + } + return res; + } + + private function group(verts:Vector., begin:int, end:int, depth:int, threshold:Number, stack:Vector.):void { + var i:int; + var j:int; + var vertex:Vertex; + var threshold:Number; + switch (depth) { + case 0: // x + for (i = begin; i < end; i++) { + vertex = verts[i]; + vertex.offset = vertex.x; + } + break; + case 1: // y + for (i = begin; i < end; i++) { + vertex = verts[i]; + vertex.offset = vertex.y; + } + break; + case 2: // z + for (i = begin; i < end; i++) { + vertex = verts[i]; + vertex.offset = vertex.z; + } + break; + } + // Sorting + stack[0] = begin; + stack[1] = end - 1; + var index:int = 2; + while (index > 0) { + index--; + var r:int = stack[index]; + j = r; + index--; + var l:int = stack[index]; + i = l; + vertex = verts[(r + l) >> 1]; + var median:Number = vertex.offset; + while (i <= j) { + var left:Vertex = verts[i]; + while (left.offset > median) { + i++; + left = verts[i]; + } + var right:Vertex = verts[j]; + while (right.offset < median) { + j--; + right = verts[j]; + } + if (i <= j) { + verts[i] = right; + verts[j] = left; + i++; + j--; + } + } + if (l < j) { + stack[index] = l; + index++; + stack[index] = j; + index++; + } + if (i < r) { + stack[index] = i; + index++; + stack[index] = r; + index++; + } + } + // Divide on groups further + i = begin; + vertex = verts[i]; + var compared:Vertex; + for (j = i + 1; j <= end; j++) { + if (j < end) compared = verts[j]; + if (j == end || vertex.offset - compared.offset > threshold) { + if (depth < 2 && j - i > 1) { + group(verts, i, j, depth + 1, threshold, stack); + } + if (j < end) { + i = j; + vertex = verts[i]; + } + } else if (depth == 2) { + compared.value = vertex; + } + } + } + + private function weldFaces(angleThreshold:Number = 0, convexThreshold:Number = 0):void { + var i:int; + var j:int; + var key:*; + var sibling:Face; + var face:Face; + var next:Face; + var wp:Wrapper; + var sp:Wrapper; + var w:Wrapper; + var s:Wrapper; + var wn:Wrapper; + var sn:Wrapper; + var wm:Wrapper; + var sm:Wrapper; + var vertex:Vertex; + var a:Vertex; + var b:Vertex; + var c:Vertex; + var abx:Number; + var aby:Number; + var abz:Number; + var acx:Number; + var acy:Number; + var acz:Number; + var nx:Number; + var ny:Number; + var nz:Number; + var nl:Number; + var dictionary:Dictionary; + // Accuracy + var digitThreshold:Number = 0.001; + angleThreshold = Math.cos(angleThreshold) - digitThreshold; + convexThreshold = Math.cos(Math.PI - convexThreshold) - digitThreshold; + // Faces + var faceSet:Dictionary = new Dictionary(); + // Map of matching vertex:faces(dictionary) + var map:Dictionary = new Dictionary(); + for (face = faceList; face != null; face = next) { + next = face.next; + face.next = null; + faceSet[face] = true; + for (wn = face.wrapper; wn != null; wn = wn.next) { + vertex = wn.vertex; + dictionary = map[vertex]; + if (dictionary == null) { + dictionary = new Dictionary(); + map[vertex] = dictionary; + } + dictionary[face] = true; + } + } + faceList = null; + // Island + var island:Vector. = new Vector.(); + // Neighbors of current edge + var siblings:Dictionary = new Dictionary(); + // Edges, that are not included to current island + var unfit:Dictionary = new Dictionary(); + while (true) { + // Get of first face + face = null; + for (key in faceSet) { + face = key; + delete faceSet[key]; + break; + } + if (face == null) break; + // Create island + var num:int = 0; + island[num] = face; + num++; + nx = face.normalX; + ny = face.normalY; + nz = face.normalZ; + for (key in unfit) { + delete unfit[key]; + } + for (i = 0; i < num; i++) { + face = island[i]; + for (key in siblings) { + delete siblings[key]; + } + // Collect potential neighbors of face + for (w = face.wrapper; w != null; w = w.next) { + for (key in map[w.vertex]) { + if (faceSet[key] && !unfit[key]) { + siblings[key] = true; + } + } + } + for (key in siblings) { + sibling = key; + // If they match along the normals + if (nx*sibling.normalX + ny*sibling.normalY + nz*sibling.normalZ >= angleThreshold) { + // Checking on the neighborhood + for (w = face.wrapper; w != null; w = w.next) { + wn = (w.next != null) ? w.next : face.wrapper; + for (s = sibling.wrapper; s != null; s = s.next) { + sn = (s.next != null) ? s.next : sibling.wrapper; + if (w.vertex == sn.vertex && wn.vertex == s.vertex) break; + } + if (s != null) break; + } + // Add to island + if (w != null) { + island[num] = sibling; + num++; + delete faceSet[sibling]; + } + } else { + unfit[sibling] = true; + } + } + } + // If island has one face + if (num == 1) { + face = island[0]; + face.next = faceList; + faceList = face; + // Unite of island + } else { + while (true) { + var weld:Boolean = false; + // Loop of island faces + for (i = 0; i < num - 1; i++) { + face = island[i]; + if (face != null) { + // Try to unite current faces with others + for (j = 1; j < num; j++) { + sibling = island[j]; + if (sibling != null) { + // Search for the common face + for (w = face.wrapper; w != null; w = w.next) { + wn = (w.next != null) ? w.next : face.wrapper; + for (s = sibling.wrapper; s != null; s = s.next) { + sn = (s.next != null) ? s.next : sibling.wrapper; + if (w.vertex == sn.vertex && wn.vertex == s.vertex) break; + } + if (s != null) break; + } + // If faces is not found + if (w != null) { + // Expansion of union faces + while (true) { + wm = (wn.next != null) ? wn.next : face.wrapper; + //for (sp = sibling.wrapper; sp.next != s && sp.next != null; sp = sp.next); + sp = sibling.wrapper; + while (sp.next != s && sp.next != null) sp = sp.next; + if (wm.vertex == sp.vertex) { + wn = wm; + s = sp; + } else break; + } + while (true) { + //for (wp = face.wrapper; wp.next != w && wp.next != null; wp = wp.next); + wp = face.wrapper; + while (wp.next != w && wp.next != null) wp = wp.next; + sm = (sn.next != null) ? sn.next : sibling.wrapper; + if (wp.vertex == sm.vertex) { + w = wp; + sn = sm; + } else break; + } + // First bend + a = w.vertex; + b = sm.vertex; + c = wp.vertex; + abx = b.x - a.x; + aby = b.y - a.y; + abz = b.z - a.z; + acx = c.x - a.x; + acy = c.y - a.y; + acz = c.z - a.z; + nx = acz*aby - acy*abz; + ny = acx*abz - acz*abx; + nz = acy*abx - acx*aby; + if (nx < digitThreshold && nx > -digitThreshold && ny < digitThreshold && ny > -digitThreshold && nz < digitThreshold && nz > -digitThreshold) { + if (abx*acx + aby*acy + abz*acz > 0) continue; + } else { + if (face.normalX*nx + face.normalY*ny + face.normalZ*nz < 0) continue; + } + nl = 1/Math.sqrt(abx*abx + aby*aby + abz*abz); + abx *= nl; + aby *= nl; + abz *= nl; + nl = 1/Math.sqrt(acx*acx + acy*acy + acz*acz); + acx *= nl; + acy *= nl; + acz *= nl; + if (abx*acx + aby*acy + abz*acz < convexThreshold) continue; + // Second bend + a = s.vertex; + b = wm.vertex; + c = sp.vertex; + abx = b.x - a.x; + aby = b.y - a.y; + abz = b.z - a.z; + acx = c.x - a.x; + acy = c.y - a.y; + acz = c.z - a.z; + nx = acz*aby - acy*abz; + ny = acx*abz - acz*abx; + nz = acy*abx - acx*aby; + if (nx < digitThreshold && nx > -digitThreshold && ny < digitThreshold && ny > -digitThreshold && nz < digitThreshold && nz > -digitThreshold) { + if (abx*acx + aby*acy + abz*acz > 0) continue; + } else { + if (face.normalX*nx + face.normalY*ny + face.normalZ*nz < 0) continue; + } + nl = 1/Math.sqrt(abx*abx + aby*aby + abz*abz); + abx *= nl; + aby *= nl; + abz *= nl; + nl = 1/Math.sqrt(acx*acx + acy*acy + acz*acz); + acx *= nl; + acy *= nl; + acz *= nl; + if (abx*acx + aby*acy + abz*acz < convexThreshold) continue; + // Unite + weld = true; + var newFace:Face = new Face(); + newFace.normalX = face.normalX; + newFace.normalY = face.normalY; + newFace.normalZ = face.normalZ; + newFace.offset = face.offset; + wm = null; + for (; wn != w; wn = (wn.next != null) ? wn.next : face.wrapper) { + sm = new Wrapper(); + sm.vertex = wn.vertex; + if (wm != null) { + wm.next = sm; + } else { + newFace.wrapper = sm; + } + wm = sm; + } + for (; sn != s; sn = (sn.next != null) ? sn.next : sibling.wrapper) { + sm = new Wrapper(); + sm.vertex = sn.vertex; + if (wm != null) { + wm.next = sm; + } else { + newFace.wrapper = sm; + } + wm = sm; + } + island[i] = newFace; + island[j] = null; + face = newFace; + // Если, то собираться будет парами, иначе к одной прицепляется максимально (это чуть быстрее) + //if (pairWeld) break; + + } + } + } + } + } + if (!weld) break; + } + // Collect of united faces + for (i = 0; i < num; i++) { + face = island[i]; + if (face != null) { + // Calculate the best sequence of vertices + face.calculateBestSequenceAndNormal(); + // Add + face.next = faceList; + faceList = face; + } + } + } + } + } + + /** + * @private + */ + alternativa3d function transformVertices(correctionX:Number, correctionY:Number):void { + for (var vertex:Vertex = vertexList; vertex != null; vertex = vertex.next) { + vertex.cameraX = (localToCameraTransform.a*vertex.x + localToCameraTransform.b*vertex.y + localToCameraTransform.c*vertex.z + localToCameraTransform.d)/correctionX; + vertex.cameraY = (localToCameraTransform.e*vertex.x + localToCameraTransform.f*vertex.y + localToCameraTransform.g*vertex.z + localToCameraTransform.h)/correctionY; + vertex.cameraZ = localToCameraTransform.i*vertex.x + localToCameraTransform.j*vertex.y + localToCameraTransform.k*vertex.z + localToCameraTransform.l; + } + } + + /** + * @private + */ + alternativa3d function checkOcclusion(occluder:Occluder, correctionX:Number, correctionY:Number):Boolean { + for (var plane:CullingPlane = occluder.planeList; plane != null; plane = plane.next) { + for (var vertex:Vertex = vertexList; vertex != null; vertex = vertex.next) { + if (plane.x*vertex.cameraX*correctionX + plane.y*vertex.cameraY*correctionY + plane.z*vertex.cameraZ > plane.offset) return false; + } + } + return true; + } + + /** + * @private + */ + alternativa3d function calculatePlanes(camera:Camera3D):void { + var a:Vertex; + var b:Vertex; + var c:Vertex; + var face:Face; + var plane:CullingPlane; + // Clear of planes + if (planeList != null) { + plane = planeList; + while (plane.next != null) plane = plane.next; + plane.next = CullingPlane.collector; + CullingPlane.collector = planeList; + planeList = null; + } + if (faceList == null || edgeList == null) return; + // Visibility of faces + if (!camera.orthographic) { + var cameraInside:Boolean = true; + for (face = faceList; face != null; face = face.next) { + if (face.normalX*cameraToLocalTransform.d + face.normalY*cameraToLocalTransform.h + face.normalZ*cameraToLocalTransform.l > face.offset) { + face.visible = true; + cameraInside = false; + } else { + face.visible = false; + } + } + if (cameraInside) return; + } else { + for (a = vertexList; a != null; a = a.next) if (a.cameraZ < camera.nearClipping) return; + for (face = faceList; face != null; face = face.next) { + face.visible = face.normalX*cameraToLocalTransform.c + face.normalY*cameraToLocalTransform.g + face.normalZ*cameraToLocalTransform.k < 0; + } + } + // Create planes by contour + var viewSizeX:Number = camera.view._width*0.5; + var viewSizeY:Number = camera.view._width*0.5; + var right:Number = viewSizeX/camera.correctionX; + var left:Number = -right; + var bottom:Number = viewSizeY/camera.correctionY; + var top:Number = -bottom; + var t:Number; + var ax:Number; + var ay:Number; + var az:Number; + var bx:Number; + var by:Number; + var bz:Number; + var ox:Number; + var oy:Number; + var lineList:CullingPlane = null; + var square:Number = 0; + var viewSquare:Number = viewSizeX*viewSizeY*4*2; + var occludeAll:Boolean = true; + for (var edge:Edge = edgeList; edge != null; edge = edge.next) { + // If face is into the contour + if (edge.left.visible != edge.right.visible) { + // Define the direction (counterclockwise) + if (edge.left.visible) { + a = edge.a; + b = edge.b; + } else { + a = edge.b; + b = edge.a; + } + ax = a.cameraX; + ay = a.cameraY; + az = a.cameraZ; + bx = b.cameraX; + by = b.cameraY; + bz = b.cameraZ; + // Clipping + if (culling > 3) { + if (!camera.orthographic) { + if (az <= -ax && bz <= -bx) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bz > -bx && az <= -ax) { + t = (ax + az)/(ax + az - bx - bz); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bz <= -bx && az > -ax) { + t = (ax + az)/(ax + az - bx - bz); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (az <= ax && bz <= bx) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bz > bx && az <= ax) { + t = (az - ax)/(az - ax + bx - bz); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bz <= bx && az > ax) { + t = (az - ax)/(az - ax + bx - bz); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (az <= -ay && bz <= -by) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bz > -by && az <= -ay) { + t = (ay + az)/(ay + az - by - bz); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bz <= -by && az > -ay) { + t = (ay + az)/(ay + az - by - bz); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (az <= ay && bz <= by) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bz > by && az <= ay) { + t = (az - ay)/(az - ay + by - bz); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bz <= by && az > ay) { + t = (az - ay)/(az - ay + by - bz); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + // Orthographic mode + } else { + if (ax <= left && bx <= left) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bx > left && ax <= left) { + t = (left - ax)/(bx - ax); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bx <= left && ax > left) { + t = (left - ax)/(bx - ax); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (ax >= right && bx >= right) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bx < right && ax >= right) { + t = (right - ax)/(bx - ax); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bx >= right && ax < right) { + t = (right - ax)/(bx - ax); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (ay <= top && by <= top) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (by > top && ay <= top) { + t = (top - ay)/(by - ay); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (by <= top && ay > top) { + t = (top - ay)/(by - ay); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (ay >= bottom && by >= bottom) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (by < bottom && ay >= bottom) { + t = (bottom - ay)/(by - ay); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (by >= bottom && ay < bottom) { + t = (bottom - ay)/(by - ay); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + } + occludeAll = false; + } + // Create plane by edge + plane = CullingPlane.create(); + plane.next = planeList; + planeList = plane; + if (!camera.orthographic) { + plane.x = (b.cameraZ*a.cameraY - b.cameraY*a.cameraZ)*camera.correctionY; + plane.y = (b.cameraX*a.cameraZ - b.cameraZ*a.cameraX)*camera.correctionX; + plane.z = (b.cameraY*a.cameraX - b.cameraX*a.cameraY)*camera.correctionX*camera.correctionY; + plane.offset = 0; + if (minSize > 0 && square/viewSquare < minSize) { + ax = ax*viewSizeX/az; + ay = ay*viewSizeY/az; + bx = bx*viewSizeX/bz; + by = by*viewSizeY/bz; + if (planeList.next == null) { + ox = ax; + oy = ay; + } + square += (bx - ox)*(ay - oy) - (by - oy)*(ax - ox); + plane = plane.create(); + plane.x = ay - by; + plane.y = bx - ax; + plane.offset = plane.x*ax + plane.y*ay; + plane.next = lineList; + lineList = plane; + } + } else { + plane.x = (a.cameraY - b.cameraY)*camera.correctionY; + plane.y = (b.cameraX - a.cameraX)*camera.correctionX; + plane.z = 0; + plane.offset = plane.x*a.cameraX*camera.correctionX + plane.y*a.cameraY*camera.correctionY; + if (minSize > 0 && square/viewSquare < minSize) { + ax = ax*camera.correctionX; + ay = ay*camera.correctionY; + bx = bx*camera.correctionX; + by = by*camera.correctionY; + if (planeList.next == null) { + ox = ax; + oy = ay; + } + square += (bx - ox)*(ay - oy) - (by - oy)*(ax - ox); + plane = plane.create(); + plane.x = ay - by; + plane.y = bx - ax; + plane.offset = plane.x*ax + plane.y*ay; + plane.next = lineList; + lineList = plane; + } + } + } + } + if (planeList == null && !occludeAll) return; + // Checking size on the display + if (planeList != null && minSize > 0 && square/viewSquare < minSize && (culling <= 3 || !checkSquare(lineList, ox, oy, square, viewSquare, viewSizeX, viewSizeY))) { + plane = planeList; + while (plane.next != null) plane = plane.next; + plane.next = CullingPlane.collector; + CullingPlane.collector = planeList; + planeList = null; + if (lineList != null) { + plane = lineList; + while (plane.next != null) plane = plane.next; + plane.next = CullingPlane.collector; + CullingPlane.collector = lineList; + } + return; + } else if (lineList != null) { + plane = lineList; + while (plane.next != null) plane = plane.next; + plane.next = CullingPlane.collector; + CullingPlane.collector = lineList; + } + // Create planes by faces. + for (face = faceList; face != null; face = face.next) { + if (!face.visible) continue; + if (culling > 3) { + occludeAll = true; + var wrapper:Wrapper; + for (wrapper = face.wrapper; wrapper != null; wrapper = wrapper.next) { + a = wrapper.vertex; + b = (wrapper.next != null) ? wrapper.next.vertex : face.wrapper.vertex; + ax = a.cameraX; + ay = a.cameraY; + az = a.cameraZ; + bx = b.cameraX; + by = b.cameraY; + bz = b.cameraZ; + if (!camera.orthographic) { + if (az <= -ax && bz <= -bx) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bz > -bx && az <= -ax) { + t = (ax + az)/(ax + az - bx - bz); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bz <= -bx && az > -ax) { + t = (ax + az)/(ax + az - bx - bz); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (az <= ax && bz <= bx) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bz > bx && az <= ax) { + t = (az - ax)/(az - ax + bx - bz); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bz <= bx && az > ax) { + t = (az - ax)/(az - ax + bx - bz); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (az <= -ay && bz <= -by) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bz > -by && az <= -ay) { + t = (ay + az)/(ay + az - by - bz); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bz <= -by && az > -ay) { + t = (ay + az)/(ay + az - by - bz); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (az <= ay && bz <= by) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bz > by && az <= ay) { + t = (az - ay)/(az - ay + by - bz); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bz <= by && az > ay) { + t = (az - ay)/(az - ay + by - bz); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + // Orthographic mode + } else { + if (ax <= left && bx <= left) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bx > left && ax <= left) { + t = (left - ax)/(bx - ax); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bx <= left && ax > left) { + t = (left - ax)/(bx - ax); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (ax >= right && bx >= right) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (bx < right && ax >= right) { + t = (right - ax)/(bx - ax); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (bx >= right && ax < right) { + t = (right - ax)/(bx - ax); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (ay <= top && by <= top) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (by > top && ay <= top) { + t = (top - ay)/(by - ay); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (by <= top && ay > top) { + t = (top - ay)/(by - ay); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + if (ay >= bottom && by >= bottom) { + if (occludeAll && by*ax - bx*ay > 0) occludeAll = false; + continue; + } else if (by < bottom && ay >= bottom) { + t = (bottom - ay)/(by - ay); + ax += (bx - ax)*t; + ay += (by - ay)*t; + az += (bz - az)*t; + } else if (by >= bottom && ay < bottom) { + t = (bottom - ay)/(by - ay); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + bz = az + (bz - az)*t; + } + } + occludeAll = false; + break; + } + if (wrapper == null && !occludeAll) continue; + } + // Create plane by face + plane = CullingPlane.create(); + plane.next = planeList; + planeList = plane; + a = face.wrapper.vertex; + b = face.wrapper.next.vertex; + c = face.wrapper.next.next.vertex; + ax = b.cameraX - a.cameraX; + ay = b.cameraY - a.cameraY; + az = b.cameraZ - a.cameraZ; + bx = c.cameraX - a.cameraX; + by = c.cameraY - a.cameraY; + bz = c.cameraZ - a.cameraZ; + plane.x = (bz*ay - by*az)*camera.correctionY; + plane.y = (bx*az - bz*ax)*camera.correctionX; + plane.z = (by*ax - bx*ay)*camera.correctionX*camera.correctionY; + plane.offset = a.cameraX*plane.x*camera.correctionX + a.cameraY*plane.y*camera.correctionY + a.cameraZ*plane.z; + } + } + + private function checkSquare(lineList:CullingPlane, ox:Number, oy:Number, square:Number, viewSquare:Number, viewSizeX:Number, viewSizeY:Number):Boolean { + var t:Number; + var ax:Number; + var ay:Number; + var ao:Number; + var bx:Number; + var by:Number; + var bo:Number; + var plane:CullingPlane; + // Clipping of viewport frame by projected contour edges + if (culling & 4) { + ax = -viewSizeX; + ay = -viewSizeY; + bx = -viewSizeX; + by = viewSizeY; + for (plane = lineList; plane != null; plane = plane.next) { + ao = ax*plane.x + ay*plane.y - plane.offset; + bo = bx*plane.x + by*plane.y - plane.offset; + if (ao < 0 || bo < 0) { + if (ao >= 0 && bo < 0) { + t = ao/(ao - bo); + ax += (bx - ax)*t; + ay += (by - ay)*t; + } else if (ao < 0 && bo >= 0) { + t = ao/(ao - bo); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + } + } else break; + } + if (plane == null) { + square += (bx - ox)*(ay - oy) - (by - oy)*(ax - ox); + if (square/viewSquare >= minSize) return true; + } + } + if (culling & 8) { + ax = viewSizeX; + ay = viewSizeY; + bx = viewSizeX; + by = -viewSizeY; + for (plane = lineList; plane != null; plane = plane.next) { + ao = ax*plane.x + ay*plane.y - plane.offset; + bo = bx*plane.x + by*plane.y - plane.offset; + if (ao < 0 || bo < 0) { + if (ao >= 0 && bo < 0) { + t = ao/(ao - bo); + ax += (bx - ax)*t; + ay += (by - ay)*t; + } else if (ao < 0 && bo >= 0) { + t = ao/(ao - bo); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + } + } else break; + } + if (plane == null) { + square += (bx - ox)*(ay - oy) - (by - oy)*(ax - ox); + if (square/viewSquare >= minSize) return true; + } + } + if (culling & 16) { + ax = viewSizeX; + ay = -viewSizeY; + bx = -viewSizeX; + by = -viewSizeY; + for (plane = lineList; plane != null; plane = plane.next) { + ao = ax*plane.x + ay*plane.y - plane.offset; + bo = bx*plane.x + by*plane.y - plane.offset; + if (ao < 0 || bo < 0) { + if (ao >= 0 && bo < 0) { + t = ao/(ao - bo); + ax += (bx - ax)*t; + ay += (by - ay)*t; + } else if (ao < 0 && bo >= 0) { + t = ao/(ao - bo); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + } + } else break; + } + if (plane == null) { + square += (bx - ox)*(ay - oy) - (by - oy)*(ax - ox); + if (square/viewSquare >= minSize) return true; + } + } + if (culling & 32) { + ax = -viewSizeX; + ay = viewSizeY; + bx = viewSizeX; + by = viewSizeY; + for (plane = lineList; plane != null; plane = plane.next) { + ao = ax*plane.x + ay*plane.y - plane.offset; + bo = bx*plane.x + by*plane.y - plane.offset; + if (ao < 0 || bo < 0) { + if (ao >= 0 && bo < 0) { + t = ao/(ao - bo); + ax += (bx - ax)*t; + ay += (by - ay)*t; + } else if (ao < 0 && bo >= 0) { + t = ao/(ao - bo); + bx = ax + (bx - ax)*t; + by = ay + (by - ay)*t; + } + } else break; + } + if (plane == null) { + square += (bx - ox)*(ay - oy) - (by - oy)*(ax - ox); + if (square/viewSquare >= minSize) return true; + } + } + return false; + } + + /** + * @private + */ + override alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + for (var vertex:Vertex = vertexList; vertex != null; vertex = vertex.next) { + var x:Number; + var y:Number; + var z:Number; + if (transform != null) { + x = transform.a*vertex.x + transform.b*vertex.y + transform.c*vertex.z + transform.d; + y = transform.e*vertex.x + transform.f*vertex.y + transform.g*vertex.z + transform.h; + z = transform.i*vertex.x + transform.j*vertex.y + transform.k*vertex.z + transform.l; + } else { + x = vertex.x; + y = vertex.y; + z = vertex.z; + } + if (x < boundBox.minX) boundBox.minX = x; + if (x > boundBox.maxX) boundBox.maxX = x; + if (y < boundBox.minY) boundBox.minY = y; + if (y > boundBox.maxY) boundBox.maxY = y; + if (z < boundBox.minZ) boundBox.minZ = z; + if (z > boundBox.maxZ) boundBox.maxZ = z; + } + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:Occluder = new Occluder(); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + var src:Occluder = source as Occluder; + minSize = src.minSize; + // Clone vertices + var vertex:Vertex; + var face:Face; + var lastVertex:Vertex; + for (vertex = src.vertexList; vertex != null; vertex = vertex.next) { + var newVertex:Vertex = new Vertex(); + newVertex.x = vertex.x; + newVertex.y = vertex.y; + newVertex.z = vertex.z; + vertex.value = newVertex; + if (lastVertex != null) { + lastVertex.next = newVertex; + } else { + vertexList = newVertex; + } + lastVertex = newVertex; + } + // Clone faces + var lastFace:Face; + for (face = src.faceList; face != null; face = face.next) { + var newFace:Face = new Face(); + newFace.normalX = face.normalX; + newFace.normalY = face.normalY; + newFace.normalZ = face.normalZ; + newFace.offset = face.offset; + face.processNext = newFace; + // Clone wrappers + var lastWrapper:Wrapper = null; + for (var wrapper:Wrapper = face.wrapper; wrapper != null; wrapper = wrapper.next) { + var newWrapper:Wrapper = new Wrapper(); + newWrapper.vertex = wrapper.vertex.value; + if (lastWrapper != null) { + lastWrapper.next = newWrapper; + } else { + newFace.wrapper = newWrapper; + } + lastWrapper = newWrapper; + } + if (lastFace != null) { + lastFace.next = newFace; + } else { + faceList = newFace; + } + lastFace = newFace; + } + // Clone edges + var lastEdge:Edge; + for (var edge:Edge = src.edgeList; edge != null; edge = edge.next) { + var newEdge:Edge = new Edge(); + newEdge.a = edge.a.value; + newEdge.b = edge.b.value; + newEdge.left = edge.left.processNext; + newEdge.right = edge.right.processNext; + if (lastEdge != null) { + lastEdge.next = newEdge; + } else { + edgeList = newEdge; + } + lastEdge = newEdge; + } + // Reset after remapping + for (vertex = src.vertexList; vertex != null; vertex = vertex.next) { + vertex.value = null; + } + for (face = src.faceList; face != null; face = face.next) { + face.processNext = null; + } + } + + } +} + +class Vertex { + + public var next:Vertex; + public var value:Vertex; + + public var x:Number; + public var y:Number; + public var z:Number; + + public var offset:Number; + + public var cameraX:Number; + public var cameraY:Number; + public var cameraZ:Number; + +} + +class Face { + + public var next:Face; + public var processNext:Face; + + public var normalX:Number; + public var normalY:Number; + public var normalZ:Number; + public var offset:Number; + + public var wrapper:Wrapper; + + public var visible:Boolean; + + public function calculateBestSequenceAndNormal():void { + if (wrapper.next.next.next != null) { + var max:Number = -1e+22; + var s:Wrapper; + var sm:Wrapper; + var sp:Wrapper; + for (w = wrapper; w != null; w = w.next) { + var wn:Wrapper = (w.next != null) ? w.next : wrapper; + var wm:Wrapper = (wn.next != null) ? wn.next : wrapper; + a = w.vertex; + b = wn.vertex; + c = wm.vertex; + abx = b.x - a.x; + aby = b.y - a.y; + abz = b.z - a.z; + acx = c.x - a.x; + acy = c.y - a.y; + acz = c.z - a.z; + nx = acz*aby - acy*abz; + ny = acx*abz - acz*abx; + nz = acy*abx - acx*aby; + nl = nx*nx + ny*ny + nz*nz; + if (nl > max) { + max = nl; + s = w; + } + } + if (s != wrapper) { + //for (sm = wrapper.next.next.next; sm.next != null; sm = sm.next); + sm = wrapper.next.next.next; + while (sm.next != null) sm = sm.next; + //for (sp = wrapper; sp.next != s && sp.next != null; sp = sp.next); + sp = wrapper; + while (sp.next != s && sp.next != null) sp = sp.next; + sm.next = wrapper; + sp.next = null; + wrapper = s; + } + } + var w:Wrapper = wrapper; + var a:Vertex = w.vertex; + w = w.next; + var b:Vertex = w.vertex; + w = w.next; + var c:Vertex = w.vertex; + var abx:Number = b.x - a.x; + var aby:Number = b.y - a.y; + var abz:Number = b.z - a.z; + var acx:Number = c.x - a.x; + var acy:Number = c.y - a.y; + var acz:Number = c.z - a.z; + var nx:Number = acz*aby - acy*abz; + var ny:Number = acx*abz - acz*abx; + var nz:Number = acy*abx - acx*aby; + var nl:Number = nx*nx + ny*ny + nz*nz; + if (nl > 0) { + nl = 1/Math.sqrt(nl); + nx *= nl; + ny *= nl; + nz *= nl; + normalX = nx; + normalY = ny; + normalZ = nz; + } + offset = a.x*nx + a.y*ny + a.z*nz; + } + +} + +class Wrapper { + + public var next:Wrapper; + + public var vertex:Vertex; + +} + +class Edge { + + public var next:Edge; + + public var a:Vertex; + public var b:Vertex; + + public var left:Face; + public var right:Face; + +} diff --git a/src/alternativa/engine3d/core/RayIntersectionData.as b/src/alternativa/engine3d/core/RayIntersectionData.as new file mode 100644 index 0000000..23c480e --- /dev/null +++ b/src/alternativa/engine3d/core/RayIntersectionData.as @@ -0,0 +1,59 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.objects.Surface; + + import flash.geom.Point; + import flash.geom.Vector3D; + + /** + * A result of searching for intersection of an Object3D and a ray with intersectRay() method of Object3D. + * + * @see Object3D#intersectRay() + */ + public class RayIntersectionData { + + /** + * First object intersected by the ray. + */ + public var object:Object3D; + + /** + * The point of intersection il local coordinates of object. + */ + public var point:Vector3D; + + /** + * Surface of object on which intersection occurred. + */ + public var surface:Surface; + + /** + * Distance from ray's origin to intersection point expressed in length of localDirection vector. + */ + public var time:Number; + + /** + * Texture coordinates of intersection point. + */ + public var uv:Point; + + /** + * Returns the string representation of the specified object. + * @return The string representation of the specified object. + */ + public function toString():String { + return "[RayIntersectionData " + object + ", " + point + ", " + uv + ", " + time + "]"; + } + + } +} diff --git a/src/alternativa/engine3d/core/Renderer.as b/src/alternativa/engine3d/core/Renderer.as new file mode 100644 index 0000000..566f661 --- /dev/null +++ b/src/alternativa/engine3d/core/Renderer.as @@ -0,0 +1,238 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.materials.ShaderProgram; + + import flash.display3D.Context3D; + import flash.display3D.Context3DCompareMode; + import flash.display3D.Context3DProgramType; + import flash.display3D.IndexBuffer3D; + import flash.display3D.Program3D; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * @private + */ + public class Renderer { + + public static const SKY:int = 10; + + public static const OPAQUE:int = 20; + + public static const DECALS:int = 30; + + public static const TRANSPARENT_SORT:int = 40; + + public static const NEXT_LAYER:int = 50; + + // Collector + protected var collector:DrawUnit; + + alternativa3d var camera:Camera3D; + + alternativa3d var drawUnits:Vector. = new Vector.(); + + // Key - context, value - properties. + protected static var properties:Dictionary = new Dictionary(true); + + protected var _context3D:Context3D; + protected var _contextProperties:RendererContext3DProperties; + + alternativa3d function render(context3D:Context3D):void { + updateContext3D(context3D); + + var drawUnitsLength:int = drawUnits.length; + for (var i:int = 0; i < drawUnitsLength; i++) { + var list:DrawUnit = drawUnits[i]; + if (list != null) { + switch (i) { + case SKY: + _context3D.setDepthTest(false, Context3DCompareMode.ALWAYS); + break; + case OPAQUE: + _context3D.setDepthTest(true, Context3DCompareMode.LESS); + break; + case DECALS: + _context3D.setDepthTest(false, Context3DCompareMode.LESS_EQUAL); + break; + case TRANSPARENT_SORT: + if (list.next != null) list = sortByAverageZ(list); + _context3D.setDepthTest(false, Context3DCompareMode.LESS); + break; + case NEXT_LAYER: + _context3D.setDepthTest(false, Context3DCompareMode.ALWAYS); + break; + } + // Rendering + while (list != null) { + var next:DrawUnit = list.next; + renderDrawUnit(list, _context3D, camera); + // Send to collector + list.clear(); + list.next = collector; + collector = list; + list = next; + } + } + } + // Clear + drawUnits.length = 0; + } + + alternativa3d function createDrawUnit(object:Object3D, program:Program3D, indexBuffer:IndexBuffer3D, firstIndex:int, numTriangles:int, debugShader:ShaderProgram = null):DrawUnit { + var res:DrawUnit; + if (collector != null) { + res = collector; + collector = collector.next; + res.next = null; + } else { + //trace("new DrawUnit"); + res = new DrawUnit(); + } + res.object = object; + res.program = program; + res.indexBuffer = indexBuffer; + res.firstIndex = firstIndex; + res.numTriangles = numTriangles; + return res; + } + + alternativa3d function addDrawUnit(drawUnit:DrawUnit, renderPriority:int):void { + // Increase array of priorities, if it is necessary + if (renderPriority >= drawUnits.length) drawUnits.length = renderPriority + 1; + // Add + drawUnit.next = drawUnits[renderPriority]; + drawUnits[renderPriority] = drawUnit; + } + + protected function renderDrawUnit(drawUnit:DrawUnit, context:Context3D, camera:Camera3D):void { + context.setBlendFactors(drawUnit.blendSource, drawUnit.blendDestination); + context.setCulling(drawUnit.culling); + var _usedBuffers:uint = _contextProperties.usedBuffers; + var _usedTextures:uint = _contextProperties.usedTextures; + + var bufferIndex:int; + var bufferBit:int; + var currentBuffers:int; + var textureSampler:int; + var textureBit:int; + var currentTextures:int; + for (var i:int = 0; i < drawUnit.vertexBuffersLength; i++) { + bufferIndex = drawUnit.vertexBuffersIndexes[i]; + bufferBit = 1 << bufferIndex; + currentBuffers |= bufferBit; + _usedBuffers &= ~bufferBit; + context.setVertexBufferAt(bufferIndex, drawUnit.vertexBuffers[i], drawUnit.vertexBuffersOffsets[i], drawUnit.vertexBuffersFormats[i]); + } + if (drawUnit.vertexConstantsRegistersCount > 0) { + context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 0, drawUnit.vertexConstants, drawUnit.vertexConstantsRegistersCount); + } + if (drawUnit.fragmentConstantsRegistersCount > 0) { + context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, drawUnit.fragmentConstants, drawUnit.fragmentConstantsRegistersCount); + } + for (i = 0; i < drawUnit.texturesLength; i++) { + textureSampler = drawUnit.texturesSamplers[i]; + textureBit = 1 << textureSampler; + currentTextures |= textureBit; + _usedTextures &= ~textureBit; + context.setTextureAt(textureSampler, drawUnit.textures[i]); + } + context.setProgram(drawUnit.program); + for (bufferIndex = 0; _usedBuffers > 0; bufferIndex++) { + bufferBit = _usedBuffers & 1; + _usedBuffers >>= 1; + if (bufferBit) context.setVertexBufferAt(bufferIndex, null); + } + for (textureSampler = 0; _usedTextures > 0; textureSampler++) { + textureBit = _usedTextures & 1; + _usedTextures >>= 1; + if (textureBit) context.setTextureAt(textureSampler, null); + } + context.drawTriangles(drawUnit.indexBuffer, drawUnit.firstIndex, drawUnit.numTriangles); + _contextProperties.usedBuffers = currentBuffers; + _contextProperties.usedTextures = currentTextures; + camera.numDraws++; + camera.numTriangles += drawUnit.numTriangles; + } + + protected function updateContext3D(value:Context3D):void { + if (_context3D != value) { + _contextProperties = properties[value]; + if (_contextProperties == null) { + _contextProperties = new RendererContext3DProperties(); + properties[value] = _contextProperties; + } + _context3D = value; + } + } + + alternativa3d function sortByAverageZ(list:DrawUnit, direction:Boolean = true):DrawUnit { + var left:DrawUnit = list; + var right:DrawUnit = list.next; + while (right != null && right.next != null) { + list = list.next; + right = right.next.next; + } + right = list.next; + list.next = null; + if (left.next != null) { + left = sortByAverageZ(left, direction); + } + if (right.next != null) { + right = sortByAverageZ(right, direction); + } + var flag:Boolean = direction ? (left.object.localToCameraTransform.l > right.object.localToCameraTransform.l) : (left.object.localToCameraTransform.l < right.object.localToCameraTransform.l); + if (flag) { + list = left; + left = left.next; + } else { + list = right; + right = right.next; + } + var last:DrawUnit = list; + while (true) { + if (left == null) { + last.next = right; + return list; + } else if (right == null) { + last.next = left; + return list; + } + if (flag) { + if (direction ? (left.object.localToCameraTransform.l > right.object.localToCameraTransform.l) : (left.object.localToCameraTransform.l < right.object.localToCameraTransform.l)) { + last = left; + left = left.next; + } else { + last.next = right; + last = right; + right = right.next; + flag = false; + } + } else { + if (direction ? (left.object.localToCameraTransform.l < right.object.localToCameraTransform.l) : (left.object.localToCameraTransform.l > right.object.localToCameraTransform.l)) { + last = right; + right = right.next; + } else { + last.next = left; + last = left; + left = left.next; + flag = true; + } + } + } + return null; + } + } +} diff --git a/src/alternativa/engine3d/core/RendererContext3DProperties.as b/src/alternativa/engine3d/core/RendererContext3DProperties.as new file mode 100644 index 0000000..a344625 --- /dev/null +++ b/src/alternativa/engine3d/core/RendererContext3DProperties.as @@ -0,0 +1,23 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + /** + * @private + * Stores settings of context. + */ + public class RendererContext3DProperties { + + public var usedBuffers:uint = 0; + public var usedTextures:uint = 0; + + } +} diff --git a/src/alternativa/engine3d/core/Resource.as b/src/alternativa/engine3d/core/Resource.as new file mode 100644 index 0000000..c66d25a --- /dev/null +++ b/src/alternativa/engine3d/core/Resource.as @@ -0,0 +1,57 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + + import flash.display3D.Context3D; + + use namespace alternativa3d; + + /** + * Base class for GPU data. GPU data can be divided in 2 groups: geometry data and texture data. + * The type of resources for uploading geometry data in GPU is Geometry. + * BitmapTextureResource allows to use textures of type is BitmapData and ATFTextureResource deals with ByteArray consists of ATF data, + * ExternalTextureResource should be used with TexturesLoader, which loads textures from files and automatically uploads in GPU. + * + * + * @see alternativa.engine3d.resources.Geometry + * @see alternativa.engine3d.resources.TextureResource + * @see alternativa.engine3d.resources.BitmapTextureResource + * @see alternativa.engine3d.resources.ATFTextureResource + * @see alternativa.engine3d.resources.ExternalTextureResource + */ + public class Resource { + + /** + * Defines if this resource is uploaded inti a Context3D. + */ + public function get isUploaded():Boolean { + return false; + } + + /** + * Uploads resource into given Context3D. + * + * @param context3D Context3D to which resource will uploaded. + */ + public function upload(context3D:Context3D):void { + throw new Error("Cannot upload without data"); + } + + /** + * Removes this resource from Context3D to which it was uploaded. + */ + public function dispose():void { + } + + } +} diff --git a/src/alternativa/engine3d/core/Transform3D.as b/src/alternativa/engine3d/core/Transform3D.as new file mode 100644 index 0000000..ba1e0f4 --- /dev/null +++ b/src/alternativa/engine3d/core/Transform3D.as @@ -0,0 +1,269 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + import alternativa.engine3d.alternativa3d; + + use namespace alternativa3d; + + /** + * @private + */ + public class Transform3D { + + public var a:Number = 1; + public var b:Number = 0; + public var c:Number = 0; + public var d:Number = 0; + + public var e:Number = 0; + public var f:Number = 1; + public var g:Number = 0; + public var h:Number = 0; + + public var i:Number = 0; + public var j:Number = 0; + public var k:Number = 1; + public var l:Number = 0; + + public function identity():void { + a = 1; + b = 0; + c = 0; + d = 0; + e = 0; + f = 1; + g = 0; + h = 0; + i = 0; + j = 0; + k = 1; + l = 0; + } + + public function compose(x:Number, y:Number, z:Number, rotationX:Number, rotationY:Number, rotationZ:Number, scaleX:Number, scaleY:Number, scaleZ:Number):void { + var cosX:Number = Math.cos(rotationX); + var sinX:Number = Math.sin(rotationX); + var cosY:Number = Math.cos(rotationY); + var sinY:Number = Math.sin(rotationY); + var cosZ:Number = Math.cos(rotationZ); + var sinZ:Number = Math.sin(rotationZ); + var cosZsinY:Number = cosZ*sinY; + var sinZsinY:Number = sinZ*sinY; + var cosYscaleX:Number = cosY*scaleX; + var sinXscaleY:Number = sinX*scaleY; + var cosXscaleY:Number = cosX*scaleY; + var cosXscaleZ:Number = cosX*scaleZ; + var sinXscaleZ:Number = sinX*scaleZ; + a = cosZ*cosYscaleX; + b = cosZsinY*sinXscaleY - sinZ*cosXscaleY; + c = cosZsinY*cosXscaleZ + sinZ*sinXscaleZ; + d = x; + e = sinZ*cosYscaleX; + f = sinZsinY*sinXscaleY + cosZ*cosXscaleY; + g = sinZsinY*cosXscaleZ - cosZ*sinXscaleZ; + h = y; + i = -sinY*scaleX; + j = cosY*sinXscaleY; + k = cosY*cosXscaleZ; + l = z; + } + + public function composeInverse(x:Number, y:Number, z:Number, rotationX:Number, rotationY:Number, rotationZ:Number, scaleX:Number, scaleY:Number, scaleZ:Number):void { + var cosX:Number = Math.cos(rotationX); + var sinX:Number = Math.sin(-rotationX); + var cosY:Number = Math.cos(rotationY); + var sinY:Number = Math.sin(-rotationY); + var cosZ:Number = Math.cos(rotationZ); + var sinZ:Number = Math.sin(-rotationZ); + var sinXsinY:Number = sinX*sinY; + var cosYscaleX:Number = cosY/scaleX; + var cosXscaleY:Number = cosX/scaleY; + var sinXscaleZ:Number = sinX/scaleZ; + var cosXscaleZ:Number = cosX/scaleZ; + a = cosZ*cosYscaleX; + b = -sinZ*cosYscaleX; + c = sinY/scaleX; + d = -a*x - b*y - c*z; + e = sinZ*cosXscaleY + sinXsinY*cosZ/scaleY; + f = cosZ*cosXscaleY - sinXsinY*sinZ/scaleY; + g = -sinX*cosY/scaleY; + h = -e*x - f*y - g*z; + i = sinZ*sinXscaleZ - cosZ*sinY*cosXscaleZ; + j = cosZ*sinXscaleZ + sinY*sinZ*cosXscaleZ; + k = cosY*cosXscaleZ; + l = -i*x - j*y - k*z; + } + + public function invert():void { + var ta:Number = a; + var tb:Number = b; + var tc:Number = c; + var td:Number = d; + var te:Number = e; + var tf:Number = f; + var tg:Number = g; + var th:Number = h; + var ti:Number = i; + var tj:Number = j; + var tk:Number = k; + var tl:Number = l; + var det:Number = 1/(-tc*tf*ti + tb*tg*ti + tc*te*tj - ta*tg*tj - tb*te*tk + ta*tf*tk); + a = (-tg*tj + tf*tk)*det; + b = (tc*tj - tb*tk)*det; + c = (-tc*tf + tb*tg)*det; + d = (td*tg*tj - tc*th*tj - td*tf*tk + tb*th*tk + tc*tf*tl - tb*tg*tl)*det; + e = (tg*ti - te*tk)*det; + f = (-tc*ti + ta*tk)*det; + g = (tc*te - ta*tg)*det; + h = (tc*th*ti - td*tg*ti + td*te*tk - ta*th*tk - tc*te*tl + ta*tg*tl)*det; + i = (-tf*ti + te*tj)*det; + j = (tb*ti - ta*tj)*det; + k = (-tb*te + ta*tf)*det; + l = (td*tf*ti - tb*th*ti - td*te*tj + ta*th*tj + tb*te*tl - ta*tf*tl)*det; + } + + public function initFromVector(vector:Vector.):void { + a = vector[0]; + b = vector[1]; + c = vector[2]; + d = vector[3]; + e = vector[4]; + f = vector[5]; + g = vector[6]; + h = vector[7]; + i = vector[8]; + j = vector[9]; + k = vector[10]; + l = vector[11]; + } + + public function append(transform:Transform3D):void { + var ta:Number = a; + var tb:Number = b; + var tc:Number = c; + var td:Number = d; + var te:Number = e; + var tf:Number = f; + var tg:Number = g; + var th:Number = h; + var ti:Number = i; + var tj:Number = j; + var tk:Number = k; + var tl:Number = l; + a = transform.a*ta + transform.b*te + transform.c*ti; + b = transform.a*tb + transform.b*tf + transform.c*tj; + c = transform.a*tc + transform.b*tg + transform.c*tk; + d = transform.a*td + transform.b*th + transform.c*tl + transform.d; + e = transform.e*ta + transform.f*te + transform.g*ti; + f = transform.e*tb + transform.f*tf + transform.g*tj; + g = transform.e*tc + transform.f*tg + transform.g*tk; + h = transform.e*td + transform.f*th + transform.g*tl + transform.h; + i = transform.i*ta + transform.j*te + transform.k*ti; + j = transform.i*tb + transform.j*tf + transform.k*tj; + k = transform.i*tc + transform.j*tg + transform.k*tk; + l = transform.i*td + transform.j*th + transform.k*tl + transform.l; + } + + public function prepend(transform:Transform3D):void { + var ta:Number = a; + var tb:Number = b; + var tc:Number = c; + var td:Number = d; + var te:Number = e; + var tf:Number = f; + var tg:Number = g; + var th:Number = h; + var ti:Number = i; + var tj:Number = j; + var tk:Number = k; + var tl:Number = l; + a = ta*transform.a + tb*transform.e + tc*transform.i; + b = ta*transform.b + tb*transform.f + tc*transform.j; + c = ta*transform.c + tb*transform.g + tc*transform.k; + d = ta*transform.d + tb*transform.h + tc*transform.l + td; + e = te*transform.a + tf*transform.e + tg*transform.i; + f = te*transform.b + tf*transform.f + tg*transform.j; + g = te*transform.c + tf*transform.g + tg*transform.k; + h = te*transform.d + tf*transform.h + tg*transform.l + th; + i = ti*transform.a + tj*transform.e + tk*transform.i; + j = ti*transform.b + tj*transform.f + tk*transform.j; + k = ti*transform.c + tj*transform.g + tk*transform.k; + l = ti*transform.d + tj*transform.h + tk*transform.l + tl; + + } + + public function combine(transformA:Transform3D, transformB:Transform3D):void { + a = transformA.a*transformB.a + transformA.b*transformB.e + transformA.c*transformB.i; + b = transformA.a*transformB.b + transformA.b*transformB.f + transformA.c*transformB.j; + c = transformA.a*transformB.c + transformA.b*transformB.g + transformA.c*transformB.k; + d = transformA.a*transformB.d + transformA.b*transformB.h + transformA.c*transformB.l + transformA.d; + e = transformA.e*transformB.a + transformA.f*transformB.e + transformA.g*transformB.i; + f = transformA.e*transformB.b + transformA.f*transformB.f + transformA.g*transformB.j; + g = transformA.e*transformB.c + transformA.f*transformB.g + transformA.g*transformB.k; + h = transformA.e*transformB.d + transformA.f*transformB.h + transformA.g*transformB.l + transformA.h; + i = transformA.i*transformB.a + transformA.j*transformB.e + transformA.k*transformB.i; + j = transformA.i*transformB.b + transformA.j*transformB.f + transformA.k*transformB.j; + k = transformA.i*transformB.c + transformA.j*transformB.g + transformA.k*transformB.k; + l = transformA.i*transformB.d + transformA.j*transformB.h + transformA.k*transformB.l + transformA.l; + } + + public function calculateInversion(source:Transform3D):void { + var ta:Number = source.a; + var tb:Number = source.b; + var tc:Number = source.c; + var td:Number = source.d; + var te:Number = source.e; + var tf:Number = source.f; + var tg:Number = source.g; + var th:Number = source.h; + var ti:Number = source.i; + var tj:Number = source.j; + var tk:Number = source.k; + var tl:Number = source.l; + var det:Number = 1/(-tc*tf*ti + tb*tg*ti + tc*te*tj - ta*tg*tj - tb*te*tk + ta*tf*tk); + a = (-tg*tj + tf*tk)*det; + b = (tc*tj - tb*tk)*det; + c = (-tc*tf + tb*tg)*det; + d = (td*tg*tj - tc*th*tj - td*tf*tk + tb*th*tk + tc*tf*tl - tb*tg*tl)*det; + e = (tg*ti - te*tk)*det; + f = (-tc*ti + ta*tk)*det; + g = (tc*te - ta*tg)*det; + h = (tc*th*ti - td*tg*ti + td*te*tk - ta*th*tk - tc*te*tl + ta*tg*tl)*det; + i = (-tf*ti + te*tj)*det; + j = (tb*ti - ta*tj)*det; + k = (-tb*te + ta*tf)*det; + l = (td*tf*ti - tb*th*ti - td*te*tj + ta*th*tj + tb*te*tl - ta*tf*tl)*det; + } + + public function copy(source:Transform3D):void { + a = source.a; + b = source.b; + c = source.c; + d = source.d; + e = source.e; + f = source.f; + g = source.g; + h = source.h; + i = source.i; + j = source.j; + k = source.k; + l = source.l; + } + + public function toString():String { + return "[Transform3D" + + " a:" + a.toFixed(3) + " b:" + b.toFixed(3) + " c:" + a.toFixed(3) + " d:" + d.toFixed(3) + + " e:" + e.toFixed(3) + " f:" + f.toFixed(3) + " g:" + a.toFixed(3) + " h:" + h.toFixed(3) + + " i:" + i.toFixed(3) + " j:" + j.toFixed(3) + " k:" + a.toFixed(3) + " l:" + l.toFixed(3) + "]"; + } + + } +} diff --git a/src/alternativa/engine3d/core/VertexAttributes.as b/src/alternativa/engine3d/core/VertexAttributes.as new file mode 100644 index 0000000..11c0ff9 --- /dev/null +++ b/src/alternativa/engine3d/core/VertexAttributes.as @@ -0,0 +1,112 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.engine3d.alternativa3d; + + import flash.display3D.Context3DVertexBufferFormat; + + use namespace alternativa3d; + + /** + * Types of attributes which defines format of vertex streams. It can be used as values of array, + * passed to geometry.addVertexStream(attributes) as an argument. + * + * @see alternativa.engine3d.resources.Geometry + */ + public class VertexAttributes { + /** + * Coordinates in 3D space. Defines by sequence of three floats. + * + * @see alternativa.engine3d.resources.Geometry + * @see #getAttributeStride() + */ + public static const POSITION:uint = 1; + /** + * Vertex normal. Defines by sequence of three floats. + * + * @see alternativa.engine3d.resources.Geometry + * @see #getAttributeStride() + */ + public static const NORMAL:uint = 2; + /** + * This data type combines values of vertex tangent and binormal within one sequence of four floats. + * The first three values defines tangent direction and the fourth can be 1 or -1 which defines to what side binormal is ordered. + * + * @see alternativa.engine3d.resources.Geometry + */ + public static const TANGENT4:uint = 3; + /** + * Data of linking of two Joints with vertex. Defines by sequence of four floats in following order: + * id of the first Joint multiplied with 3, power of influence of the first Joint, + * id of the second Joint multiplied with 3, power of influence of the second Joint. + * There are a four 'slots' for this data type, so influence of 8 Joints can be described. + * @see alternativa.engine3d.resources.Geometry + * @see alternativa.engine3d.objects.Skin + */ + public static const JOINTS:Vector. = Vector.([4,5,6,7]); + + /** + * Texture coordinates data type. There are a 8 independent channels. Coordinates defines by the couples (u, v). + * + * @see alternativa.engine3d.resources.Geometry + */ + public static const TEXCOORDS:Vector. = Vector.([8,9,10,11,12,13,14,15]); + + /** + * @private + */ + alternativa3d static const FORMATS:Array = [ + Context3DVertexBufferFormat.FLOAT_1, //NONE + Context3DVertexBufferFormat.FLOAT_3, //POSITION + Context3DVertexBufferFormat.FLOAT_3, //NORMAL + Context3DVertexBufferFormat.FLOAT_4, //TANGENT4 + Context3DVertexBufferFormat.FLOAT_4, //JOINTS[0] + Context3DVertexBufferFormat.FLOAT_4, //JOINTS[1] + Context3DVertexBufferFormat.FLOAT_4, //JOINTS[2] + Context3DVertexBufferFormat.FLOAT_4, //JOINTS[3] + Context3DVertexBufferFormat.FLOAT_2, //TEXCOORDS[0] + Context3DVertexBufferFormat.FLOAT_2, //TEXCOORDS[1] + Context3DVertexBufferFormat.FLOAT_2, //TEXCOORDS[2] + Context3DVertexBufferFormat.FLOAT_2, //TEXCOORDS[3] + Context3DVertexBufferFormat.FLOAT_2, //TEXCOORDS[4] + Context3DVertexBufferFormat.FLOAT_2, //TEXCOORDS[5] + Context3DVertexBufferFormat.FLOAT_2, //TEXCOORDS[6] + Context3DVertexBufferFormat.FLOAT_2 //TEXCOORDS[7] + ]; + + /** + * Returns a dimensions of given attribute type (Number of floats by which defines given type) + * + * @param attribute Type of the attribute. + * @return + */ + public static function getAttributeStride(attribute:int):int { + switch(FORMATS[attribute]) { + case Context3DVertexBufferFormat.FLOAT_1: + return 1; + break; + case Context3DVertexBufferFormat.FLOAT_2: + return 2; + break; + case Context3DVertexBufferFormat.FLOAT_3: + return 3; + break; + case Context3DVertexBufferFormat.FLOAT_4: + return 4; + break; + } + return 0; + } + + + } +} diff --git a/src/alternativa/engine3d/core/VertexStream.as b/src/alternativa/engine3d/core/VertexStream.as new file mode 100644 index 0000000..54afeaa --- /dev/null +++ b/src/alternativa/engine3d/core/VertexStream.as @@ -0,0 +1,24 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import flash.display3D.VertexBuffer3D; + import flash.utils.ByteArray; + + /** + * @private + */ + public class VertexStream { + public var buffer:VertexBuffer3D; + public var attributes:Array; + public var data:ByteArray; + } +} diff --git a/src/alternativa/engine3d/core/View.as b/src/alternativa/engine3d/core/View.as new file mode 100644 index 0000000..3a5e0a0 --- /dev/null +++ b/src/alternativa/engine3d/core/View.as @@ -0,0 +1,1432 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core { + + import alternativa.Alternativa3D; + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.events.MouseEvent3D; + import alternativa.engine3d.materials.ShaderProgram; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.Geometry; + + import flash.display.Bitmap; + import flash.display.BitmapData; + import flash.display.DisplayObject; + import flash.display.Sprite; + import flash.display.Stage3D; + import flash.display.StageAlign; + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DCompareMode; + import flash.display3D.Context3DProgramType; + import flash.display3D.Context3DTriangleFace; + import flash.display3D.VertexBuffer3D; + import flash.events.ContextMenuEvent; + import flash.events.Event; + import flash.events.KeyboardEvent; + import flash.events.MouseEvent; + import flash.geom.Point; + import flash.geom.Rectangle; + import flash.geom.Vector3D; + import flash.net.URLRequest; + import flash.net.navigateToURL; + import flash.ui.ContextMenu; + import flash.ui.ContextMenuItem; + import flash.ui.Keyboard; + import flash.ui.Mouse; + import flash.utils.Dictionary; + import flash.utils.setTimeout; + + use namespace alternativa3d; + + /** + * A viewport. Though GPU can render to one of Stage3D only, View still extends DisplayObject and should be in DisplayList. + * Since 8 version of Alternativa3D view used as wrapper for configuring Stage3D properties for first. Main task of view is defining + * rectangular field of screen to which image will be rendered. Another opportunity is render image to Bitmap. In this case the view + * will have this Bitmap as a child. The size of View should be 50x50 at least. + * In case of size will be more than 2048 and anti-aliasing is turned on, usage of MouseEvents will cause of crash. + * + * @see alternativa.engine3d.core.Camera3D + */ + public class View extends Sprite { + + private static const renderEvent:MouseEvent = new MouseEvent("render"); + + private static var properties:Dictionary = new Dictionary(true); + private var cachedContext3D:Context3D; + private var context3DViewProperties:Context3DViewProperties; + + static private var drawDistanceFragment:Linker; + static private var drawDistanceVertexProcedure:Procedure; + + static private const drawUnit:DrawUnit = new DrawUnit(); + static private const pixels:Dictionary = new Dictionary(); + static private const stack:Vector. = new Vector.(); + + static private const point:Point = new Point(); + static private const scissor:Rectangle = new Rectangle(0, 0, 1, 1); + static private const localCoords:Vector3D = new Vector3D(); + + static private const branch:Vector. = new Vector.(); + static private const overedBranch:Vector. = new Vector.(); + static private const changedBranch:Vector. = new Vector.(); + static private const functions:Vector. = new Vector.(); + + private static const drawColoredRectConst:Vector. = Vector.([0, 0, -1, 1]); + private static const drawRectColor:Vector. = new Vector.(4); + + /** + * Background color. + */ + public var backgroundColor:uint; + + /** + * Background transparency. + */ + public var backgroundAlpha:Number; + + /** + * Level of anti-aliasing. + */ + public var antiAlias:int; + + /** + * @private + */ + alternativa3d var _width:int; + + /** + * @private + */ + alternativa3d var _height:int; + + private var backBufferContext3D:Context3D; + private var backBufferWidth:int = -1; + private var backBufferHeight:int = -1; + private var backBufferAntiAlias:int = -1; + + /** + * @private + */ + alternativa3d var _canvas:BitmapData = null; + + /** + * Mouse events occurred over this View since last render. + */ + private var events:Vector. = new Vector.(); + /** + * Indices of rays in the raysOrigins array for each mouse event. + */ + private var indices:Vector. = new Vector.(); + private var eventsLength:int = 0; + + // Surfaces of objects which can be crossed by mouse and procedures of transformation their coordinates + + private var surfaces:Vector. = new Vector.(); + private var geometries:Vector. = new Vector.(); + private var procedures:Vector. = new Vector.(); + private var surfacesLength:int = 0; + + /** + * @private + */ + alternativa3d var raysOrigins:Vector. = new Vector.(); + /** + * @private + */ + alternativa3d var raysDirections:Vector. = new Vector.(); + private var raysCoefficients:Vector. = new Vector.(); + private var raysSurfaces:Vector.> = new Vector.>(); + private var raysDepths:Vector.> = new Vector.>(); + private var raysIs:Vector. = new Vector.(); + private var raysJs:Vector. = new Vector.(); + + /** + * @private + */ + alternativa3d var raysLength:int = 0; + + private var lastEvent:MouseEvent; + + private var target:Object3D; + private var targetSurface:Surface; + private var targetDepth:Number; + private var pressedTarget:Object3D; + private var clickedTarget:Object3D; + private var overedTarget:Object3D; + private var overedTargetSurface:Surface; + + private var altKey:Boolean; + private var ctrlKey:Boolean; + private var shiftKey:Boolean; + + private var container:Bitmap; + private var area:Sprite; + private var logo:Logo; + private var bitmap:Bitmap; + private var _logoAlign:String = "BR"; + private var _logoHorizontalMargin:Number = 0; + private var _logoVerticalMargin:Number = 0; + private var _renderToBitmap:Boolean; + + /** + * Creates a View object. + * @param width Width of a view, should be 50 at least. + * @param height Height of a view, should be 50 at least. + * @param renderToBitmap If true, image will render to Bitmap object which will included into the view as a child. It also will available through canvas property. + * @param backgroundColor Background color. + * @param backgroundAlpha BAckground transparency. + * @param antiAlias Level of anti-aliasing. + * + * @see #canvas + */ + public function View(width:int, height:int, renderToBitmap:Boolean = false, backgroundColor:uint = 0, backgroundAlpha:Number = 1, antiAlias:int = 0) { + if (width < 50) width = 50; + if (height < 50) height = 50; + _width = width; + _height = height; + _renderToBitmap = renderToBitmap; + this.backgroundColor = backgroundColor; + this.backgroundAlpha = backgroundAlpha; + this.antiAlias = antiAlias; + + mouseEnabled = true; + mouseChildren = true; + doubleClickEnabled = true; + + buttonMode = true; + useHandCursor = false; + + tabEnabled = false; + tabChildren = false; + + // Context menu + var item:ContextMenuItem = new ContextMenuItem("Powered by Alternativa3D " + Alternativa3D.version); + item.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, function(e:ContextMenuEvent):void { + try { + navigateToURL(new URLRequest("http://alternativaplatform.com"), "_blank"); + } catch (e:Error) { + } + }); + var menu:ContextMenu = new ContextMenu(); + menu.customItems = [item]; + contextMenu = menu; + + // Canvas + container = new Bitmap(); + if (renderToBitmap) { + createRenderBitmap(); + } + super.addChild(container); + + // Hit area + area = new Sprite(); + area.graphics.beginFill(0xFF0000); + area.graphics.drawRect(0, 0, 100, 100); + area.mouseEnabled = false; + area.visible = false; + area.width = _width; + area.height = _height; + hitArea = area; + super.addChild(hitArea); + + // Logo + showLogo(); + + if (drawDistanceFragment == null) { + drawDistanceVertexProcedure = Procedure.compileFromArray([ + // Declaraion + "#v0=distance", + "#c0=transform0", + "#c1=transform1", + "#c2=transform2", + "#c3=coefficient", + "#c4=projection", + // Convert to the camera coordinates + "dp4 t0.x, i0, c0", + "dp4 t0.y, i0, c1", + "dp4 t0.z, i0, c2", + // Passing the depth + "mul v0.x, t0.z, c3.z", + "mov v0.y, i0.x", + "mov v0.z, i0.x", + "mov v0.w, i0.x", + // Projection + "mul t1.x, t0.x, c4.x", + "mul t1.y, t0.y, c4.y", + "mul t0.w, t0.z, c4.z", + "add t1.z, t0.w, c4.w", + // Get last line + "mov t3.z, c4.x", + "div t3.z, t3.z, c4.x", + "sub t3.z, t3.z, c3.w", + // Finding W + "mul t1.w, t0.z, t3.z", + "add t1.w, t1.w, c3.w", + // Offset + "mul t0.x, c3.x, t1.w", + "mul t0.y, c3.y, t1.w", + "add t1.x, t1.x, t0.x", + "add t1.y, t1.y, t0.y", + "mov o0, t1", + ], "mouseEventsVertex"); + drawDistanceFragment = new Linker(Context3DProgramType.FRAGMENT); + drawDistanceFragment.addProcedure(new Procedure([ + // Id + "mov t0.z, c0.z", + // An unit + "mov t0.w, c0.w", + // Remainder + "frc t0.y, v0.x", + // A whole part + "sub t0.x, v0.x, t0.y", + "mul t0.x, t0.x, c0.x", + "mov o0, ft0", + // Declaration + "#v0=distance", + "#c0=code", + ], "mouseEventsFragment")); + } + + // Listeners + addEventListener(MouseEvent.MOUSE_DOWN, onMouse); + addEventListener(MouseEvent.CLICK, onMouse); + addEventListener(MouseEvent.DOUBLE_CLICK, onMouse); + addEventListener(MouseEvent.MOUSE_MOVE, onMouse); + addEventListener(MouseEvent.MOUSE_OVER, onMouse); + addEventListener(MouseEvent.MOUSE_WHEEL, onMouse); + addEventListener(MouseEvent.MOUSE_OUT, onLeave); + addEventListener(Event.ADDED_TO_STAGE, onAddToStage); + addEventListener(Event.REMOVED_FROM_STAGE, onRemoveFromStage); + } + + private function onMouse(mouseEvent:MouseEvent):void { + var prev:int = eventsLength - 1; + // case of mouseMove repeats + if (eventsLength > 0 && mouseEvent.type == "mouseMove" && (events[prev] as MouseEvent).type == "mouseMove") { + events[prev] = mouseEvent; + } else { + events[eventsLength] = mouseEvent; + eventsLength++; + } + lastEvent = mouseEvent; + } + + private function onLeave(mouseEvent:MouseEvent):void { + events[eventsLength] = mouseEvent; + eventsLength++; + lastEvent = null; + } + + private function createRenderBitmap():void { + _canvas = new BitmapData(_width, _height, backgroundAlpha < 1, backgroundColor); + container.bitmapData = _canvas; + container.smoothing = true; + } + + private function onAddToStage(e:Event):void { + stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); + stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); + } + + private function onRemoveFromStage(e:Event):void { + stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); + stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp); + altKey = false; + ctrlKey = false; + shiftKey = false; + } + + private function onKeyDown(keyboardEvent:KeyboardEvent):void { + altKey = keyboardEvent.altKey; + ctrlKey = keyboardEvent.ctrlKey; + shiftKey = keyboardEvent.shiftKey; + if (ctrlKey && shiftKey && keyboardEvent.keyCode == Keyboard.F1 && bitmap == null) { + bitmap = new Bitmap(Logo.image); + bitmap.x = Math.round((_width - bitmap.width)/2); + bitmap.y = Math.round((_height - bitmap.height)/2); + super.addChild(bitmap); + setTimeout(removeBitmap, 2048); + } + } + + private function onKeyUp(keyboardEvent:KeyboardEvent):void { + altKey = keyboardEvent.altKey; + ctrlKey = keyboardEvent.ctrlKey; + shiftKey = keyboardEvent.shiftKey; + } + + private function removeBitmap():void { + if (bitmap != null) { + super.removeChild(bitmap); + bitmap = null; + } + } + + /** + * @private + */ + alternativa3d function calculateRays(camera:Camera3D):void { + var i:int; + var mouseEvent:MouseEvent; + // Case of last coordinates fits in the view. + if (lastEvent != null) { + // Detecting mouse movement within the frame + var mouseMoved:Boolean = false; + for (i = 0; i < eventsLength; i++) { + mouseEvent = events[i]; + if (mouseEvent.type == "mouseMove" || mouseEvent.type == "mouseOut") { + mouseMoved = true; + break; + } + } + // Add event of checking if content over mouse was changed + if (!mouseMoved) { + renderEvent.localX = lastEvent.localX; + renderEvent.localY = lastEvent.localY; + renderEvent.ctrlKey = ctrlKey; + renderEvent.altKey = altKey; + renderEvent.shiftKey = shiftKey; + renderEvent.buttonDown = lastEvent.buttonDown; + renderEvent.delta = 0; + events[eventsLength] = renderEvent; + eventsLength++; + } + } + + // Creation of exclusive rays + var mouseX:Number = 1e+22; + var mouseY:Number = 1e+22; + for (i = 0; i < eventsLength; i++) { + mouseEvent = events[i]; + if (mouseEvent.type != "mouseOut") { + // Calculation of ray within the camera + if (mouseEvent.localX != mouseX || mouseEvent.localY != mouseY) { + mouseX = mouseEvent.localX; + mouseY = mouseEvent.localY; + // Creation + var origin:Vector3D; + var direction:Vector3D; + var coefficient:Point; + if (raysLength < raysOrigins.length) { + origin = raysOrigins[raysLength]; + direction = raysDirections[raysLength]; + coefficient = raysCoefficients[raysLength]; + } else { + origin = new Vector3D(); + direction = new Vector3D(); + coefficient = new Point(); + raysOrigins[raysLength] = origin; + raysDirections[raysLength] = direction; + raysCoefficients[raysLength] = coefficient; + raysSurfaces[raysLength] = new Vector.(); + raysDepths[raysLength] = new Vector.(); + } + // Filling + if (!camera.orthographic) { + direction.x = mouseX - _width*0.5; + direction.y = mouseY - _height*0.5; + direction.z = camera.focalLength; + origin.x = direction.x*camera.nearClipping/camera.focalLength; + origin.y = direction.y*camera.nearClipping/camera.focalLength; + origin.z = camera.nearClipping; + direction.normalize(); + coefficient.x = mouseX*2/_width; + coefficient.y = mouseY*2/_height; + } else { + direction.x = 0; + direction.y = 0; + direction.z = 1; + origin.x = mouseX - _width*0.5; + origin.y = mouseY - _height*0.5; + origin.z = camera.nearClipping; + coefficient.x = mouseX*2/_width; + coefficient.y = mouseY*2/_height; + } + raysLength++; + } + // Considering event with the ray + indices[i] = raysLength - 1; + } else { + indices[i] = -1; + } + } + } + + /** + * @private + */ + alternativa3d function addSurfaceToMouseEvents(surface:Surface, geometry:Geometry, procedure:Procedure):void { + surfaces[surfacesLength] = surface; + geometries[surfacesLength] = geometry; + procedures[surfacesLength] = procedure; + surfacesLength++; + } + + /** + * @private + */ + alternativa3d function prepareToRender(stage3D:Stage3D, context:Context3D):void { + if (_canvas == null) { + var vis:Boolean = this.visible; + for (var parent:DisplayObject = this.parent; parent != null; parent = parent.parent) { + vis &&= parent.visible; + } + var coords:Point; + point.x = 0; + point.y = 0; + coords = localToGlobal(point); + stage3D.x = coords.x; + stage3D.y = coords.y; + stage3D.visible = vis; + } else { + stage3D.visible = false; + if (_width != _canvas.width || _height != _canvas.height || (backgroundAlpha < 1) != _canvas.transparent) { + _canvas.dispose(); + createRenderBitmap(); + } + } + if (_width != backBufferWidth || _height != backBufferHeight || antiAlias != backBufferAntiAlias || context != backBufferContext3D) { + backBufferWidth = _width; + backBufferHeight = _height; + backBufferAntiAlias = antiAlias; + backBufferContext3D = context; + context.configureBackBuffer(_width, _height, antiAlias); + } + var r:Number = ((backgroundColor >> 16) & 0xff)/0xff; + var g:Number = ((backgroundColor >> 8) & 0xff)/0xff; + var b:Number = (backgroundColor & 0xff)/0xff; + if (canvas != null) { + r *= backgroundAlpha; + g *= backgroundAlpha; + b *= backgroundAlpha; + } + context.clear(r, g, b, backgroundAlpha); + } + + /** + * @private + */ + alternativa3d function processMouseEvents(context:Context3D, camera:Camera3D):void { + var i:int; + // Mouse events + if (eventsLength > 0) { + if (surfacesLength > 0) { + // Calculating the depth + calculateSurfacesDepths(context, camera, _width, _height); + // Sorting by decreasing the depth + for (i = 0; i < raysLength; i++) { + var raySurfaces:Vector. = raysSurfaces[i]; + var rayDepths:Vector. = raysDepths[i]; + var raySurfacesLength:int = raySurfaces.length; + if (raySurfacesLength > 1) { + sort(raySurfaces, rayDepths, raySurfacesLength); + } + } + } + // Event handling + targetDepth = camera.farClipping; + for (i = 0; i < eventsLength; i++) { + var mouseEvent:MouseEvent = events[i]; + var index:int = indices[i]; + // Check event type + switch (mouseEvent.type) { + case "mouseDown": + defineTarget(index); + if (target != null) { + propagateEvent(MouseEvent3D.MOUSE_DOWN, mouseEvent, camera, target, targetSurface, branchToVector(target, branch)); + } + pressedTarget = target; + break; + case "mouseWheel": + defineTarget(index); + if (target != null) { + propagateEvent(MouseEvent3D.MOUSE_WHEEL, mouseEvent, camera, target, targetSurface, branchToVector(target, branch)); + } + break; + case "click": + defineTarget(index); + if (target != null) { + propagateEvent(MouseEvent3D.MOUSE_UP, mouseEvent, camera, target, targetSurface, branchToVector(target, branch)); + if (pressedTarget == target) { + clickedTarget = target; + propagateEvent(MouseEvent3D.CLICK, mouseEvent, camera, target, targetSurface, branchToVector(target, branch)); + } + } + pressedTarget = null; + break; + case "doubleClick": + defineTarget(index); + if (target != null) { + propagateEvent(MouseEvent3D.MOUSE_UP, mouseEvent, camera, target, targetSurface, branchToVector(target, branch)); + if (pressedTarget == target) { + propagateEvent(clickedTarget == target && target.doubleClickEnabled ? MouseEvent3D.DOUBLE_CLICK : MouseEvent3D.CLICK, mouseEvent, camera, target, targetSurface, branchToVector(target, branch)); + } + } + clickedTarget = null; + pressedTarget = null; + break; + case "mouseMove": + defineTarget(index); + if (target != null) { + propagateEvent(MouseEvent3D.MOUSE_MOVE, mouseEvent, camera, target, targetSurface, branchToVector(target, branch)); + } + if (overedTarget != target) { + processOverOut(mouseEvent, camera); + } + break; + case "mouseOut": + lastEvent = null; + target = null; + targetSurface = null; + if (overedTarget != target) { + processOverOut(mouseEvent, camera); + } + break; + case "render": + defineTarget(index); + if (overedTarget != target) { + processOverOut(mouseEvent, camera); + } + break; + } + target = null; + targetSurface = null; + targetDepth = camera.farClipping; + } + } + // Reset surfaces + surfaces.length = 0; + surfacesLength = 0; + // Reset events + events.length = 0; + eventsLength = 0; + // Reset rays + for (i = 0; i < raysLength; i++) { + raysSurfaces[i].length = 0; + raysDepths[i].length = 0; + } + raysLength = 0; + } + + /** + * Calculates depth of every ray to every surface and writes it to the rayDepths property + * + * @param context + * @param camera + * @param contextWidth + * @param contextHeight + */ + private function calculateSurfacesDepths(context:Context3D, camera:Camera3D, contextWidth:int, contextHeight:int):void { + // Clear + context.setBlendFactors(Context3DBlendFactor.ONE, Context3DBlendFactor.ZERO); + context.setCulling(Context3DTriangleFace.FRONT); + context.setTextureAt(0, null); + context.setTextureAt(1, null); + context.setTextureAt(2, null); + context.setTextureAt(3, null); + context.setTextureAt(4, null); + context.setTextureAt(5, null); + context.setTextureAt(6, null); + context.setTextureAt(7, null); + context.setVertexBufferAt(0, null); + context.setVertexBufferAt(1, null); + context.setVertexBufferAt(2, null); + context.setVertexBufferAt(3, null); + context.setVertexBufferAt(4, null); + context.setVertexBufferAt(5, null); + context.setVertexBufferAt(6, null); + context.setVertexBufferAt(7, null); + + if (context != cachedContext3D) { + // Get properties. + cachedContext3D = context; + context3DViewProperties = properties[cachedContext3D]; + if (context3DViewProperties == null) { + context3DViewProperties = new Context3DViewProperties(); + var rectGeometry:Geometry = new Geometry(4); + rectGeometry.addVertexStream([VertexAttributes.POSITION, VertexAttributes.POSITION, VertexAttributes.POSITION, VertexAttributes.TEXCOORDS[0], VertexAttributes.TEXCOORDS[0]]); + rectGeometry.setAttributeValues(VertexAttributes.POSITION, Vector.([0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1])); + rectGeometry.setAttributeValues(VertexAttributes.TEXCOORDS[0], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + rectGeometry.indices = Vector.([0, 1, 3, 2, 3, 1]); + rectGeometry.upload(context); + vLinker = new Linker(Context3DProgramType.VERTEX); + vLinker.addProcedure(Procedure.compileFromArray([ + "#a0=a0", + "#c0=c0", + "mul t0.x, a0.x, c0.x", + "mul t0.y, a0.y, c0.y", + "add o0.x, t0.x, c0.z", + "add o0.y, t0.y, c0.w", + "mov o0.z, a0.z", + "mov o0.w, a0.z", + ])); + fLinker = new Linker(Context3DProgramType.FRAGMENT); + fLinker.addProcedure(Procedure.compileFromArray([ + "#c0=c0", + "mov o0, c0", + ])); + var coloredRectProgram:ShaderProgram = new ShaderProgram(vLinker, fLinker); + coloredRectProgram.upload(context); + + context3DViewProperties.drawRectGeometry = rectGeometry; + context3DViewProperties.drawColoredRectProgram = coloredRectProgram; + properties[cachedContext3D] = context3DViewProperties; + } + } + var drawRectGeometry:Geometry = context3DViewProperties.drawRectGeometry; + var drawColoredRectProgram:ShaderProgram = context3DViewProperties.drawColoredRectProgram; + + // Rectangle + var vLinker:Linker, fLinker:Linker; + + // Constants + var m0:Number = camera.m0; + var m5:Number = camera.m5; + var m10:Number = camera.m10; + var m11:Number = camera.m14; + var kZ:Number = 255/camera.farClipping; + var fragmentConst:Number = 1/255; + + // Loop the unique rays + var i:int; + var j:int; + var pixelIndex:int = 0; + + for (i = 0; i < raysLength; i++) { + var rayCoefficients:Point = raysCoefficients[i]; + // Draws the surface of the ray + for (j = 0; j < surfacesLength; j++) { + if (pixelIndex == 0) { + // Set constants + drawColoredRectConst[0] = raysLength*surfacesLength*2/contextWidth; + drawColoredRectConst[1] = -2/contextHeight; + // Fill background with blue color + context.setDepthTest(false, Context3DCompareMode.ALWAYS); + context.setProgram(drawColoredRectProgram.program); + context.setVertexBufferAt(0, drawRectGeometry.getVertexBuffer(VertexAttributes.POSITION), drawRectGeometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 0, drawColoredRectConst); + drawRectColor[0] = 0; + drawRectColor[1] = 0; + drawRectColor[2] = 1; + drawRectColor[3] = 1; + context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, drawRectColor); + context.drawTriangles(drawRectGeometry._indexBuffer, 0, 2); + context.setVertexBufferAt(0, null); + context.setDepthTest(true, Context3DCompareMode.LESS); + } + scissor.x = pixelIndex; + context.setScissorRectangle(scissor); + drawSurface(context, camera, j, m0, m5, m10, m11, (pixelIndex*2/contextWidth - rayCoefficients.x), rayCoefficients.y, kZ, fragmentConst, camera.orthographic); + raysIs[pixelIndex] = i; + raysJs[pixelIndex] = j; + pixelIndex++; + if (pixelIndex >= contextWidth || i >= raysLength - 1 && j >= surfacesLength - 1) { + // get + var pixel:BitmapData = pixels[pixelIndex]; + if (pixel == null) { + pixel = new BitmapData(pixelIndex, 1, false, 0xFF); + pixels[pixelIndex] = pixel; + } + context.drawToBitmapData(pixel); + for (var k:int = 0; k < pixelIndex; k++) { + var color:int = pixel.getPixel(k, 0); + var red:int = (color >> 16) & 0xFF; + var green:int = (color >> 8) & 0xFF; + var blue:int = color & 0xFF; + if (blue == 0) { + var ind:int = raysIs[k]; + var raySurfaces:Vector. = raysSurfaces[ind]; + var rayDepths:Vector. = raysDepths[ind]; + ind = raysJs[k]; + raySurfaces.push(surfaces[ind]); + rayDepths.push((red + green/255)/kZ); + } + } + pixelIndex = 0; + } + } + } + context.setScissorRectangle(null); + + // Overlaying by background color + context.setDepthTest(true, Context3DCompareMode.ALWAYS); + context.setProgram(drawColoredRectProgram.program); + context.setVertexBufferAt(0, drawRectGeometry.getVertexBuffer(VertexAttributes.POSITION), drawRectGeometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + drawColoredRectConst[0] = raysLength*surfacesLength*2/contextWidth; + drawColoredRectConst[1] = -2/contextHeight; + context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 0, drawColoredRectConst); + var r:Number = ((backgroundColor >> 16) & 0xff)/0xff; + var g:Number = ((backgroundColor >> 8) & 0xff)/0xff; + var b:Number = (backgroundColor & 0xff)/0xff; + if (canvas != null) { + drawRectColor[0] = backgroundAlpha*r; + drawRectColor[1] = backgroundAlpha*g; + drawRectColor[2] = backgroundAlpha*b; + } else { + drawRectColor[0] = r; + drawRectColor[1] = g; + drawRectColor[2] = b; + } + drawRectColor[3] = backgroundAlpha; + context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, drawRectColor); + context.drawTriangles(drawRectGeometry._indexBuffer, 0, 2); + context.setVertexBufferAt(0, null); + } + + private function drawSurface(context:Context3D, camera:Camera3D, index:int, m0:Number, m5:Number, m10:Number, m14:Number, xOffset:Number, yOffset:Number, vertexConst:Number, fragmentConst:Number, orthographic:Boolean):void { + // Surface + var surface:Surface = surfaces[index]; + var geometry:Geometry = geometries[index]; + var procedure:Procedure = procedures[index]; + var object:Object3D = surface.object; + // Program + var drawDistanceProgram:ShaderProgram = context3DViewProperties.drawDistancePrograms[procedure]; + if (drawDistanceProgram == null) { + // Assembling the vertex shader + var vertex:Linker = new Linker(Context3DProgramType.VERTEX); + var position:String = "position"; + vertex.declareVariable(position, VariableType.ATTRIBUTE); + if (procedure != null) { + vertex.addProcedure(procedure); + vertex.declareVariable("localPosition", VariableType.TEMPORARY); + vertex.setInputParams(procedure, position); + vertex.setOutputParams(procedure, "localPosition"); + position = "localPosition"; + } + vertex.addProcedure(drawDistanceVertexProcedure); + vertex.setInputParams(drawDistanceVertexProcedure, position); + // Assembling the prgram + drawDistanceProgram = new ShaderProgram(vertex, drawDistanceFragment); + drawDistanceProgram.fragmentShader.varyings = drawDistanceProgram.vertexShader.varyings; + drawDistanceProgram.upload(context); + context3DViewProperties.drawDistancePrograms[procedure] = drawDistanceProgram; + } + var buffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + if (buffer == null) return; + // Draw call (it is required for setting constants only) + drawUnit.vertexBuffersLength = 0; + drawUnit.vertexConstantsRegistersCount = 0; + drawUnit.fragmentConstantsRegistersCount = 0; + object.setTransformConstants(drawUnit, surface, drawDistanceProgram.vertexShader, camera); + drawUnit.setVertexConstantsFromTransform(drawDistanceProgram.vertexShader.getVariableIndex("transform0"), object.localToCameraTransform); + drawUnit.setVertexConstantsFromNumbers(drawDistanceProgram.vertexShader.getVariableIndex("coefficient"), xOffset, yOffset, vertexConst, orthographic ? 1 : 0); + drawUnit.setVertexConstantsFromNumbers(drawDistanceProgram.vertexShader.getVariableIndex("projection"), m0, m5, m10, m14); + drawUnit.setFragmentConstantsFromNumbers(drawDistanceProgram.fragmentShader.getVariableIndex("code"), fragmentConst, 0, 0, 1); + context.setProgram(drawDistanceProgram.program); + // Buffers + var i:int; + context.setVertexBufferAt(0, buffer, geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + for (i = 0; i < drawUnit.vertexBuffersLength; i++) { + context.setVertexBufferAt(drawUnit.vertexBuffersIndexes[i], drawUnit.vertexBuffers[i], drawUnit.vertexBuffersOffsets[i], drawUnit.vertexBuffersFormats[i]); + } + context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 0, drawUnit.vertexConstants, drawUnit.vertexConstantsRegistersCount); + context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, drawUnit.fragmentConstants, drawUnit.fragmentConstantsRegistersCount); + context.drawTriangles(geometry._indexBuffer, surface.indexBegin, surface.numTriangles); + // Clearing + context.setVertexBufferAt(0, null); + for (i = 0; i < drawUnit.vertexBuffersLength; i++) { + context.setVertexBufferAt(drawUnit.vertexBuffersIndexes[i], null); + } + } + + private function sort(surfaces:Vector., depths:Vector., length:int):void { + stack[0] = 0; + stack[1] = length - 1; + var index:int = 2; + while (index > 0) { + index--; + var r:int = stack[index]; + var j:int = r; + index--; + var l:int = stack[index]; + var i:int = l; + var median:Number = depths[(r + l) >> 1]; + while (i <= j) { + var left:Number = depths[i]; + while (left > median) { + i++; + left = depths[i]; + } + var right:Number = depths[j]; + while (right < median) { + j--; + right = depths[j]; + } + if (i <= j) { + depths[i] = right; + depths[j] = left; + var surface:Surface = surfaces[i]; + surfaces[i] = surfaces[j]; + surfaces[j] = surface; + i++; + j--; + } + } + if (l < j) { + stack[index] = l; + index++; + stack[index] = j; + index++; + } + if (i < r) { + stack[index] = i; + index++; + stack[index] = r; + index++; + } + } + } + + private function processOverOut(mouseEvent:MouseEvent, camera:Camera3D):void { + branchToVector(target, branch); + branchToVector(overedTarget, overedBranch); + var branchLength:int = branch.length; + var overedBranchLength:int = overedBranch.length; + var changedBranchLength:int; + var i:int; + var j:int; + var object:Object3D; + if (overedTarget != null) { + propagateEvent(MouseEvent3D.MOUSE_OUT, mouseEvent, camera, overedTarget, overedTargetSurface, overedBranch, true, target); + changedBranchLength = 0; + for (i = 0; i < overedBranchLength; i++) { + object = overedBranch[i]; + for (j = 0; j < branchLength; j++) if (object == branch[j]) break; + if (j == branchLength) { + changedBranch[changedBranchLength] = object; + changedBranchLength++; + } + } + if (changedBranchLength > 0) { + changedBranch.length = changedBranchLength; + propagateEvent(MouseEvent3D.ROLL_OUT, mouseEvent, camera, overedTarget, overedTargetSurface, changedBranch, false, target); + } + } + if (target != null) { + changedBranchLength = 0; + for (i = 0; i < branchLength; i++) { + object = branch[i]; + for (j = 0; j < overedBranchLength; j++) if (object == overedBranch[j]) break; + if (j == overedBranchLength) { + changedBranch[changedBranchLength] = object; + changedBranchLength++; + } + } + if (changedBranchLength > 0) { + changedBranch.length = changedBranchLength; + propagateEvent(MouseEvent3D.ROLL_OVER, mouseEvent, camera, target, targetSurface, changedBranch, false, overedTarget); + } + propagateEvent(MouseEvent3D.MOUSE_OVER, mouseEvent, camera, target, targetSurface, branch, true, overedTarget); + useHandCursor = target.useHandCursor; + } else { + useHandCursor = false; + } + Mouse.cursor = Mouse.cursor; + overedTarget = target; + overedTargetSurface = targetSurface; + } + + private function branchToVector(object:Object3D, vector:Vector.):Vector. { + var len:int = 0; + while (object != null) { + vector[len] = object; + len++; + object = object._parent; + } + vector.length = len; + return vector; + } + + private function propagateEvent(type:String, mouseEvent:MouseEvent, camera:Camera3D, target:Object3D, targetSurface:Surface, objects:Vector., bubbles:Boolean = true, relatedObject:Object3D = null):void { + var oblectsLength:int = objects.length; + var object:Object3D; + var vector:Vector.; + var length:int; + var i:int; + var j:int; + var mouseEvent3D:MouseEvent3D; + // Capture + for (i = oblectsLength - 1; i > 0; i--) { + object = objects[i]; + if (object.captureListeners != null) { + vector = object.captureListeners[type]; + if (vector != null) { + if (mouseEvent3D == null) { + calculateLocalCoords(camera, target.cameraToLocalTransform, targetDepth, mouseEvent); + mouseEvent3D = new MouseEvent3D(type, bubbles, localCoords.x, localCoords.y, localCoords.z, relatedObject, mouseEvent.ctrlKey, mouseEvent.altKey, mouseEvent.shiftKey, mouseEvent.buttonDown, mouseEvent.delta); + mouseEvent3D._target = target; + mouseEvent3D._surface = targetSurface; + } + mouseEvent3D._currentTarget = object; + mouseEvent3D._eventPhase = 1; + length = vector.length; + for (j = 0; j < length; j++) functions[j] = vector[j]; + for (j = 0; j < length; j++) { + (functions[j] as Function).call(null, mouseEvent3D); + if (mouseEvent3D.stopImmediate) return; + } + if (mouseEvent3D.stop) return; + } + } + } + // Bubble + for (i = 0; i < oblectsLength; i++) { + object = objects[i]; + if (object.bubbleListeners != null) { + vector = object.bubbleListeners[type]; + if (vector != null) { + if (mouseEvent3D == null) { + calculateLocalCoords(camera, target.cameraToLocalTransform, targetDepth, mouseEvent); + mouseEvent3D = new MouseEvent3D(type, bubbles, localCoords.x, localCoords.y, localCoords.z, relatedObject, mouseEvent.ctrlKey, mouseEvent.altKey, mouseEvent.shiftKey, mouseEvent.buttonDown, mouseEvent.delta); + mouseEvent3D._target = target; + mouseEvent3D._surface = targetSurface; + } + mouseEvent3D._currentTarget = object; + mouseEvent3D._eventPhase = (i == 0) ? 2 : 3; + length = vector.length; + for (j = 0; j < length; j++) functions[j] = vector[j]; + for (j = 0; j < length; j++) { + (functions[j] as Function).call(null, mouseEvent3D); + if (mouseEvent3D.stopImmediate) return; + } + if (mouseEvent3D.stop) return; + } + } + } + } + + private function calculateLocalCoords(camera:Camera3D, transform:Transform3D, z:Number, mouseEvent:MouseEvent):void { + var x:Number; + var y:Number; + if (!camera.orthographic) { + x = z*(mouseEvent.localX - _width*0.5)/camera.focalLength; + y = z*(mouseEvent.localY - _height*0.5)/camera.focalLength; + } else { + x = mouseEvent.localX - _width*0.5; + y = mouseEvent.localY - _height*0.5; + } + localCoords.x = transform.a*x + transform.b*y + transform.c*z + transform.d; + localCoords.y = transform.e*x + transform.f*y + transform.g*z + transform.h; + localCoords.z = transform.i*x + transform.j*y + transform.k*z + transform.l; + } + + private function defineTarget(index:int):void { + var source:Object3D; + // Get surfaces + var surfaces:Vector. = raysSurfaces[index]; + var depths:Vector. = raysDepths[index]; + // Loop surfaces + for (var i:int = surfaces.length - 1; i >= 0; i--) { + var surface:Surface = surfaces[i]; + var depth:Number = depths[i]; + var object:Object3D = surface.object; + var potentialTarget:Object3D = null; + var obj:Object3D; + // Get possible target + for (obj = object; obj != null; obj = obj._parent) { + if (!obj.mouseChildren) potentialTarget = null; + if (potentialTarget == null && obj.mouseEnabled) potentialTarget = obj; + } + // If possible target found + if (potentialTarget != null) { + if (target != null) { + for (obj = potentialTarget; obj != null; obj = obj._parent) { + if (obj == target) { + source = object; + if (target != potentialTarget) { + target = potentialTarget; + targetSurface = surface; + targetDepth = depth; + } + break; + } + } + } else { + source = object; + target = potentialTarget; + targetSurface = surface; + targetDepth = depth; + } + if (source == target) break; + } + } + } + + /** + * If true, image will render to Bitmap object which will included into the view as a child. It also will available through canvas property. + * + * @see #canvas + */ + public function get renderToBitmap():Boolean { + return _canvas != null; + } + + /** + * @private + */ + public function set renderToBitmap(value:Boolean):void { + if (value) { + if (_canvas == null) createRenderBitmap(); + } else { + if (_canvas != null) { + container.bitmapData = null; + _canvas.dispose(); + _canvas = null; + } + } + } + + /** + * BitmapData with rendered image in case of renderToBitmap turned on. + * + * @see #renderToBitmap + */ + public function get canvas():BitmapData { + return _canvas; + } + + /** + * Places Alternativa3D logo into the view. + */ + public function showLogo():void { + if (logo == null) { + logo = new Logo(); + super.addChild(logo); + resizeLogo(); + } + } + + /** + * Places Alternativa3D logo from the view. + */ + public function hideLogo():void { + if (logo != null) { + super.removeChild(logo); + logo = null; + } + } + + /** + * Alinging the logo. Constants of StageAlign class can be used as a value to set. + */ + public function get logoAlign():String { + return _logoAlign; + } + + /** + * @private + */ + public function set logoAlign(value:String):void { + _logoAlign = value; + resizeLogo(); + } + + /** + * Horizontal margin. + */ + public function get logoHorizontalMargin():Number { + return _logoHorizontalMargin; + } + + /** + * @private + */ + public function set logoHorizontalMargin(value:Number):void { + _logoHorizontalMargin = value; + resizeLogo(); + } + + /** + * Vertical margin. + */ + public function get logoVerticalMargin():Number { + return _logoVerticalMargin; + } + + /** + * @private + */ + public function set logoVerticalMargin(value:Number):void { + _logoVerticalMargin = value; + resizeLogo(); + } + + private function resizeLogo():void { + if (logo != null) { + if (_logoAlign == StageAlign.TOP_LEFT || _logoAlign == StageAlign.LEFT || _logoAlign == StageAlign.BOTTOM_LEFT) { + logo.x = Math.round(_logoHorizontalMargin); + } + if (_logoAlign == StageAlign.TOP || _logoAlign == StageAlign.BOTTOM) { + logo.x = Math.round((_width - logo.width)/2); + } + if (_logoAlign == StageAlign.TOP_RIGHT || _logoAlign == StageAlign.RIGHT || _logoAlign == StageAlign.BOTTOM_RIGHT) { + logo.x = Math.round(_width - _logoHorizontalMargin - logo.width); + } + if (_logoAlign == StageAlign.TOP_LEFT || _logoAlign == StageAlign.TOP || _logoAlign == StageAlign.TOP_RIGHT) { + logo.y = Math.round(_logoVerticalMargin); + } + if (_logoAlign == StageAlign.LEFT || _logoAlign == StageAlign.RIGHT) { + logo.y = Math.round((_height - logo.height)/2); + } + if (_logoAlign == StageAlign.BOTTOM_LEFT || _logoAlign == StageAlign.BOTTOM || _logoAlign == StageAlign.BOTTOM_RIGHT) { + logo.y = Math.round(_height - _logoVerticalMargin - logo.height); + } + } + } + + /** + * Width of this View. Should be 50 at least. + */ + override public function get width():Number { + return _width; + } + + /** + * @private + */ + override public function set width(value:Number):void { + if (value < 50) value = 50; + _width = value; + area.width = value; + resizeLogo(); + } + + /** + * Height of this View. Should be 50 at least. + */ + override public function get height():Number { + return _height; + } + + /** + * @private + */ + override public function set height(value:Number):void { + if (value < 50) value = 50; + _height = value; + area.height = value; + resizeLogo(); + } + + /** + * @private + */ + override public function addChild(child:DisplayObject):DisplayObject { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function removeChild(child:DisplayObject):DisplayObject { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function addChildAt(child:DisplayObject, index:int):DisplayObject { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function removeChildAt(index:int):DisplayObject { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function removeChildren(beginIndex:int = 0, endIndex:int = 2147483647):void { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function getChildAt(index:int):DisplayObject { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function getChildIndex(child:DisplayObject):int { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function setChildIndex(child:DisplayObject, index:int):void { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function swapChildren(child1:DisplayObject, child2:DisplayObject):void { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function swapChildrenAt(index1:int, index2:int):void { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function get numChildren():int { + return 0; + } + + /** + * @private + */ + override public function getChildByName(name:String):DisplayObject { + throw new Error("Unsupported operation."); + } + + /** + * @private + */ + override public function contains(child:DisplayObject):Boolean { + throw new Error("Unsupported operation."); + } + + } +} + +import alternativa.engine3d.materials.ShaderProgram; +import alternativa.engine3d.resources.Geometry; + +import flash.display.BitmapData; +import flash.display.Sprite; +import flash.events.MouseEvent; +import flash.geom.ColorTransform; +import flash.geom.Matrix; +import flash.net.URLRequest; +import flash.net.navigateToURL; +import flash.utils.Dictionary; + +class Logo extends Sprite { + + static public const image:BitmapData = createBMP(); + + static private function createBMP():BitmapData { + + var bmp:BitmapData = new BitmapData(165, 27, true, 0); + + bmp.setVector(bmp.rect, Vector.([ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,134217728,503316480,721420288,503316480,134217728,134217728,503316480,721420288,503316480,134217728,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100663296,419430400,721420288,788529152,536870912,234881024,50331648,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,503316480,1677721600,2348810240,1677721600,503316480,503316480,1677721600,2348810240,1677721600,503316480,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,67108864,301989888,822083584,1677721600,2365587456,2483027968,1996488704,1241513984,536870912,117440512,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16777216,167772160,520093696,822083584,905969664,822083584,520093696,301989888,520093696,822083584,905969664,822083584,620756992,620756992,721420288,620756992,620756992,721420288,620756992,620756992,721420288,620756992,620756992,822083584,905969664,822083584,520093696,218103808,234881024,536870912,721420288,620756992,620756992,822083584,905969664,822083584,520093696,301989888,520093696,822083584,1493172224,2768240640,4292467161,2533359616,822083584,822083584,2533359616,4292467161,2768240640,1493172224,822083584,620756992,620756992,721420288,503316480,268435456,503316480,721420288,503316480,134217728,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,134217728,620756992,1392508928,2248146944,3514129719,4192520610,4277921461,3886715221,2905283846,1778384896,788529152,234881024,50331648,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,167772160,822083584,1845493760,2533359616,2734686208,2533359616,1845493760,1325400064,1845493760,2533359616,2734686208,2533359616,2164260864,2164260864,2348810240,2164260864,2164260864,2348810240,2164260864,2164260864,2348810240,2164260864,2164260864,2533359616,2734686208,2533359616,1845493760,1056964608,1107296256,1895825408,2348810240,2164260864,2164260864,2533359616,2734686208,2533359616,1845493760,1325400064,1845493760,2533359616,2952790016,3730463322,4292467161,2734686208,905969664,905969664,2734686208,4292467161,3730463322,2952790016,2533359616,2164260864,2164260864,2348810240,1677721600,989855744,1677721600,2348810240,1677721600,503316480,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,16777216,167772160,754974720,1828716544,3022988562,4022445697,4294959283,4294953296,4294953534,4294961056,4226733479,3463135252,2130706432,1224736768,486539264,83886080,0,0,0,0,0,0,0,0,0,0,0,0,0,0,520093696,1845493760,3665591420,4292467161,4292467161,4292467161,3665591420,2650800128,3665591420,4292467161,4292467161,4292467161,3816191606,3355443200,4292467161,3355443200,3355443200,4292467161,3355443200,3355443200,4292467161,3355443200,3816191606,4292467161,4292467161,4292467161,3665591420,2382364672,2415919104,3801125008,4292467161,3355443200,3816191606,4292467161,4292467161,4292467161,3495911263,2650800128,3665591420,4292467161,4292467161,4292467161,4292467161,2533359616,822083584,822083584,2533359616,4292467161,4292467161,4292467161,4292467161,3816191606,3355443200,4292467161,2533359616,1627389952,2533359616,4292467161,2533359616,822083584,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,50331648,251658240,889192448,1962934272,3463338042,4260681651,4294955128,4294949388,4294949120,4294948864,4294948864,4294953816,4294960063,3903219779,2701722370,1627389952,620756992,100663296,0,0,0,0,0,0,0,0,0,0,0,0,0,822083584,2533359616,4292467161,3730463322,3187671040,3730463322,4292467161,3456106496,4292467161,3849680245,3221225472,3849680245,4292467161,3640655872,4292467161,3640655872,3640655872,4292467161,3640655872,3640655872,4292467161,3640655872,4292467161,3966923378,3640655872,3966923378,4292467161,3355443200,3918236555,4292467161,3763951961,3539992576,4292467161,3966923378,3640655872,3966923378,4292467161,3456106496,4292467161,3849680245,3221225472,3422552064,3456106496,2348810240,721420288,721420288,2348810240,3456106496,3422552064,3221225472,3849680245,4292467161,3640655872,4292467161,2734686208,1828716544,2734686208,4292467161,2734686208,905969664,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,50331648,318767104,1006632960,2080374784,3683940948,4294958002,4294949951,4294946816,4294946048,4294944256,4294944256,4294945536,4294944512,4294944799,4294954914,4123823487,3056010753,1778384896,671088640,117440512,0,0,0,0,0,0,0,0,0,0,0,0,822083584,2533359616,4292467161,3187671040,2734686208,3187671040,4292467161,3640655872,4292467161,3221225472,2801795072,3221225472,4292467161,3640655872,4292467161,3966923378,3640655872,4292467161,3966923378,3640655872,4292467161,3640655872,4292467161,3640655872,4292467161,4292467161,4292467161,3640655872,4292467161,3613154396,2818572288,3221225472,4292467161,3640655872,4292467161,4292467161,4292467161,3640655872,4292467161,3221225472,2801795072,3221225472,4292467161,2533359616,822083584,822083584,2533359616,4292467161,3221225472,2801795072,3221225472,4292467161,3640655872,4292467161,2952790016,2264924160,2952790016,4292467161,2533359616,822083584,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,50331648,318767104,1056964608,2147483648,3819605095,4294955172,4294944795,4294943744,4294941184,4294939392,4294940672,4294940160,4294938624,4294941440,4294940672,4294936323,4294815095,4208955271,3208382211,1845493760,721420288,134217728,0,0,0,0,0,0,0,0,0,0,0,721420288,2348810240,3456106496,3405774848,3187671040,3730463322,4292467161,3456106496,4292467161,3849680245,3221225472,3849680245,4292467161,3355443200,3816191606,4292467161,3966923378,3966923378,4292467161,3966923378,4292467161,3640655872,4292467161,3966923378,3640655872,3640655872,3640655872,3640655872,4292467161,2868903936,1996488704,2684354560,4292467161,3966923378,3640655872,3640655872,3539992576,3456106496,4292467161,3849680245,3221225472,3849680245,4292467161,2533359616,822083584,822083584,2533359616,4292467161,3849680245,3221225472,3849680245,4292467161,3456106496,4292467161,3730463322,3187671040,3405774848,3456106496,2348810240,721420288,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,16777216,234881024,989855744,2147483648,3836647021,4294952084,4294939916,4294939392,4294936064,4294935808,4294939907,3970992676,3783616794,4260594952,4294933248,4294937088,4294937088,4294865664,4294676569,4243165579,3292924164,1862270976,721420288,134217728,0,0,0,0,0,0,0,0,0,0,822083584,2533359616,4292467161,4292467161,4292467161,4292467161,3665591420,2650800128,3665591420,4292467161,4292467161,4292467161,3665591420,2348810240,2348810240,3665591420,4292467161,3355443200,3816191606,4292467161,4292467161,3355443200,3816191606,4292467161,4292467161,4292467161,3696908890,3355443200,4292467161,2533359616,1325400064,1845493760,3665591420,4292467161,4292467161,4292467161,3665591420,2650800128,3665591420,4292467161,4292467161,4292467161,3665591420,1845493760,520093696,520093696,1845493760,3665591420,4292467161,4292467161,4292467161,3665591420,2650800128,3665591420,4292467161,4292467161,4292467161,4292467161,2533359616,822083584,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,150994944,855638016,2063597568,3785853032,4294949263,4294935301,4294934528,4294931200,4294865408,4294739211,3598869795,2348810240,2248146944,3157861897,4158024716,4294930432,4294934272,4294934016,4294796032,4294604868,4260400774,3309963524,1862270976,704643072,117440512,0,0,0,0,0,0,0,0,0,905969664,2734686208,4292467161,3730463322,2952790016,2533359616,1845493760,1325400064,1845493760,2533359616,2734686208,2533359616,1845493760,1006632960,1006632960,1845493760,2348810240,2164260864,2164260864,2533359616,2533359616,2164260864,2164260864,2533359616,2734686208,2533359616,2164260864,2164260864,2348810240,1677721600,671088640,822083584,1845493760,2533359616,2734686208,2533359616,1845493760,1325400064,1845493760,2533359616,2734686208,2533359616,1845493760,822083584,167772160,167772160,822083584,1845493760,2533359616,2734686208,2533359616,1845493760,1325400064,1845493760,2533359616,2952790016,3730463322,4292467161,2734686208,905969664,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,117440512,738197504,1962934272,3632951638,4294947982,4294931462,4294930176,4294794752,4294662144,4260327185,3378071325,1946157056,922746880,822083584,1677721600,2785937666,3954400527,4294929408,4294931968,4294931712,4294661120,4294469180,4260200571,3208316675,1795162112,620756992,83886080,0,0,0,0,0,0,0,0,822083584,2533359616,4292467161,2768240640,1493172224,822083584,520093696,301989888,520093696,822083584,905969664,822083584,520093696,184549376,184549376,520093696,721420288,620756992,620756992,822083584,822083584,620756992,620756992,822083584,905969664,822083584,620756992,620756992,721420288,503316480,150994944,167772160,520093696,822083584,905969664,822083584,520093696,301989888,520093696,822083584,905969664,822083584,520093696,167772160,16777216,16777216,167772160,520093696,822083584,905969664,822083584,520093696,301989888,520093696,822083584,1493172224,2768240640,4292467161,2533359616,822083584,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,67108864,620756992,1811939328,3429059385,4294882972,4294796301,4294727936,4294526208,4294325760,4226241553,3242276118,1862270976,738197504,150994944,100663296,520093696,1325400064,2264924160,3768667144,4294928385,4294929408,4294796800,4294460416,4294335293,4225986666,3055813377,1644167168,503316480,50331648,0,0,0,0,0,0,0,503316480,1677721600,2348810240,1677721600,503316480,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,503316480,1677721600,2348810240,1677721600,503316480,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,16777216,335544320,1459617792,3005750036,4243500445,4294661403,4294524672,4294258432,4294121728,4259985678,3259118102,1845493760,704643072,134217728,0,0,50331648,335544320,1006632960,2080374784,3751757574,4294794241,4294794240,4294592771,4294323463,4294400588,4123811671,2769158144,1275068416,251658240,0,0,0,0,0,0,0,134217728,503316480,721420288,503316480,134217728,0,0,0,0,134217728,503316480,721420288,503316480,268435456,503316480,721420288,503316480,134217728,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,134217728,503316480,721420288,503316480,134217728,0,0,0,0,134217728,503316480,721420288,503316480,134217728,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,134217728,503316480,721420288,520093696,167772160,16777216,0,0,0,0,0,0,0,0,134217728,503316480,721420288,520093696,234881024,285212672,570425344,687865856,436207616,117440512,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,150994944,922746880,2348810240,4056321414,4294197820,4294119936,4294056448,4293921536,4293991688,3394978333,1879048192,704643072,117440512,0,0,0,0,33554432,268435456,1023410176,2248146944,3869450497,4293927168,4293661957,4293331976,4293330946,4293609799,3936365867,2181038080,822083584,134217728,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,503316480,1677721600,2348810240,1744830464,1140850688,1744830464,2348810240,1744830464,637534208,67108864,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,503316480,1677721600,2348810240,1744830464,637534208,67108864,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,503316480,1677721600,2348810240,1811939328,771751936,150994944,0,0,0,0,0,0,0,0,503316480,1677721600,2348810240,1811939328,1040187392,1207959552,1979711488,2248146944,1509949440,436207616,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,50331648,620756992,1879048192,3649264467,4294272360,4293853184,4293920000,4293920000,4293918720,3649195041,1979711488,754974720,134217728,0,0,0,0,0,67108864,335544320,1023410176,2080374784,3036676096,4088070144,4292476928,4292608000,4292739072,4292804608,4293347915,3581022738,1879048192,654311424,83886080,0,0,0,0,0,0,0,0,50331648,201326592,335544320,201326592,50331648,0,822083584,2533359616,4294967295,3261885548,2080374784,2768240640,4294967295,3261885548,1258291200,234881024,117440512,402653184,671088640,687865856,469762048,184549376,33554432,0,83886080,318767104,419430400,352321536,469762048,620756992,620756992,520093696,335544320,150994944,50331648,0,0,50331648,201326592,335544320,201326592,50331648,0,822083584,2533359616,4294967295,3295439980,1610612736,872415232,520093696,318767104,301989888,167772160,33554432,0,33554432,167772160,301989888,167772160,33554432,50331648,201326592,335544320,201326592,50331648,0,0,0,50331648,184549376,469762048,704643072,704643072,469762048,184549376,855638016,2533359616,4294809856,3566287616,1493172224,335544320,0,0,50331648,234881024,402653184,234881024,50331648,0,822083584,2550136832,4294809856,3583064832,2147483648,2382364672,3921236224,4209802240,2181038080,687865856,184549376,469762048,704643072,704643072,469762048,184549376,50331648,50331648,234881024,520093696,671088640,704643072,822083584,889192448,771751936,721420288,805306368,771751936,520093696,234881024,50331648,0, + 0,0,0,268435456,1358954496,3023117852,4260334217,4293854213,4293919488,4293921024,4293853184,4055516443,2348810240,939524096,150994944,0,0,0,0,33554432,201326592,671088640,1442840576,2264924160,3513790764,3356295425,3473866752,4207017984,4292673536,4292804608,4292870144,4292937479,4276240705,3174499075,1610612736,419430400,0,0,0,0,0,0,0,83886080,452984832,1157627904,1577058304,1174405120,486539264,83886080,905969664,2734686208,4294967295,3479528805,2533359616,3087007744,4294967295,3429394536,1543503872,520093696,754974720,1610612736,2248146944,2298478592,1845493760,1107296256,385875968,150994944,587202560,1409286144,1644167168,1442840576,1761607680,2147483648,2147483648,1962934272,1593835520,1040187392,385875968,50331648,83886080,452984832,1157627904,1577058304,1174405120,486539264,234881024,1191182336,2868903936,4294967295,3630326370,2734686208,2432696320,1962934272,1526726656,1392508928,822083584,167772160,0,167772160,822083584,1392508928,1073741824,436207616,503316480,1157627904,1577058304,1174405120,486539264,83886080,0,83886080,452984832,1140850688,1845493760,2315255808,2315255808,1845493760,1140850688,1375731712,2818572288,4294804480,3666292992,1744830464,419430400,0,100663296,520093696,1275068416,1694498816,1291845632,536870912,234881024,1191182336,2868903936,4294804480,3783471360,2952790016,3768006912,4294606336,3681495040,2130706432,1023410176,1140850688,1845493760,2315255808,2315255808,1845493760,1140850688,469762048,335544320,1006632960,1879048192,2248146944,2298478592,2533359616,2667577344,2449473536,2332033024,2499805184,2449473536,1962934272,1191182336,419430400,50331648, + 0,0,83886080,754974720,2181038080,3971250292,4293995053,4293853184,4293855488,4293591040,4208406034,2938050314,1426063360,335544320,16777216,0,0,50331648,234881024,620756992,1207959552,2013265920,3107396370,4055000155,4293554803,4003672881,3221225472,3544186880,4292673536,4292804608,4292870144,4292870144,4293006099,4122616354,2600796160,1023410176,134217728,0,0,0,0,0,67108864,520093696,1560281088,2685209869,3768886436,2736133654,1644167168,603979776,1006632960,2734686208,4294967295,3630326370,2952790016,3630326370,4294967295,3429394536,1711276032,1191182336,1979711488,3531768450,4140814287,4140945873,3801519766,2584349194,1493172224,1006632960,1728053248,3380576127,3769610159,2734686208,3752175013,4022386880,4022386880,3886721706,3513806960,2739028546,1140850688,285212672,520093696,1560281088,2685209869,3768886436,2736133654,1644167168,1107296256,1996488704,3579994722,4294967295,3780992349,3456106496,4294967295,3596837731,3173130786,3600916897,1828716544,553648128,50331648,553648128,1828716544,3600916897,2620009002,1509949440,1694498816,2685209869,3768886436,2736133654,1644167168,603979776,150994944,503316480,1543503872,2889486848,3767873792,4175254272,4175254272,3767873792,2889486848,2399141888,3120562176,4294798592,3632408576,1694498816,402653184,83886080,587202560,1644167168,2786133248,3870512384,2870872320,1711276032,1140850688,2013265920,3682543616,4294798592,3866764800,3456106496,4294798592,4054399232,3338665984,2516582400,2097152000,2889486848,3767873792,4175254272,4175254272,3767873792,2889486848,1660944384,1275068416,2097152000,3836689152,4192360448,3422552064,4294798592,4294798592,4192360448,3970513152,4294798592,4192360448,3903601152,2788757760,1174405120,234881024, + 0,0,335544320,1493172224,3259970090,4294206305,4293591040,4293263872,4292935936,4292806915,3648730389,1862270976,620756992,117440512,117440512,285212672,553648128,922746880,1392508928,2063597568,2938704652,3834466116,4276189301,4292948534,4293067009,4293740360,3732214294,3187671040,3918004480,4293132288,4293066752,4292935680,4292870144,4293073434,3681683208,1879048192,536870912,33554432,0,0,0,16777216,402653184,1509949440,2869890831,4106733511,4277729528,4157920468,3022135842,1644167168,1392508928,2751463424,4294967295,3730726494,3271557120,4294967295,4294967295,3429394536,2030043136,2147483648,3768820643,4209699562,3832574064,3815599469,4260820726,3970410407,2936999695,2499805184,3464001656,4022189501,3578678862,3355443200,4294967295,3747569503,3426828609,3426828609,4004030632,3784347792,1996488704,922746880,1509949440,2869890831,4106733511,4277729528,4157920468,3022135842,2348810240,2768240640,4294967295,4294967295,3847969627,3640655872,4294967295,3847969627,4020084125,4260820726,2888510251,1157627904,335544320,1157627904,2888510251,4260820726,3852772516,2734686208,3004174352,4106733511,4277729528,4157920468,3022135842,1644167168,721420288,1275068416,3058897920,4106697728,4294726912,4020186368,4020186368,4294726912,4106697728,3561427200,3489660928,4294792448,3598458880,1660944384,402653184,452984832,1577058304,2937455616,4158017536,4277752576,4192228608,3090025216,2382364672,2801795072,4294792448,4294792448,3916570368,3640655872,4294792448,4294792448,4294792448,3170893824,3460961024,4106697728,4294726912,4020186368,4020186368,4294726912,4106697728,3259962368,2617245696,3649906432,4277752576,3730839552,3539992576,4294792448,3849592320,3679458560,4277752576,4037094656,3813479168,4277752576,3886493184,1996488704,536870912, + 0,67108864,788529152,2281701376,4072894821,4293201424,4292870144,4292804608,4292608000,4156898324,2634022912,1392508928,822083584,905969664,1140850688,1476395008,2013265920,2617573888,3292798484,3902098491,4241976422,4292888908,4292806923,4293001216,4293263360,4293525760,4294260515,3510376966,3305308160,4191289856,4293394432,4293066752,4292935680,4293001473,4242215445,2871133184,1191182336,201326592,0,0,0,268435456,1291845632,2701723913,4072652735,4260754933,3782702967,4175355614,4158446812,2870877726,2264924160,3019898880,4294967295,3630326370,2952790016,3305111552,4294967295,3462817382,2399141888,3243660886,4277795321,3764675684,3372220416,3405774848,3780071247,4174829270,3729542220,3456106496,4294967295,3801256594,2885681152,3271557120,4294967295,3546506083,2298478592,2348810240,3648616825,4294967295,2818572288,2097152000,2701723913,4072652735,4260754933,3782702967,4175355614,4158446812,3289979161,3137339392,3456106496,4294967295,3847969627,3640655872,4294967295,3780992349,3645064003,4226674157,3767833748,1996488704,1174405120,1996488704,3767833748,4243385580,3712107074,3473278470,4072652735,4260754933,3782702967,4175355614,4158446812,2870877726,1711276032,1811939328,3685360896,4124521216,3410759424,2920416000,2920416000,3427536640,4157944576,4105775616,3640655872,4294787072,3564509184,1627389952,637534208,1342177280,2785739264,4106956544,4260773120,3867745792,4209325824,4192286208,3323987200,3137339392,3456106496,4294787072,3899463168,3640655872,4294787072,3798931456,3204448256,3221225472,4055509504,4157944576,3427536640,2920416000,2920416000,3427536640,4157944576,4072286720,3456106496,4294787072,3886162944,2969567232,3305111552,4294787072,3698464768,2952790016,3783794432,4294787072,3640655872,3951172864,4294787072,2550136832,822083584, + 0,285212672,1426063360,3277007389,4293737023,4293066752,4292870144,4292739072,4241565196,3423471106,2717908992,2197815296,2264924160,2685010432,3005286916,3377536014,3851039012,4139536965,4292959323,4292818747,4292742414,4292804608,4292804608,4292935680,4293725440,4294590464,3954466066,2871071238,2466250752,3445623808,4294119168,4293525504,4293132288,4293001216,4293462024,3835439624,2030043136,654311424,67108864,0,50331648,788529152,2348941826,3851061898,4260886519,3529005144,3120562176,3525452322,4243451373,3902446234,3288926473,3439329280,4294967295,3513017444,2634022912,3137339392,4294967295,3496306021,2583691264,3717567893,4209041632,3489660928,4141406424,4141538010,4124168657,4192461795,3868365458,3523215360,4294967295,3462817382,2499805184,3070230528,4294967295,3429394536,1811939328,1845493760,3446040166,4294967295,3422552064,3221225472,3851061898,4260886519,3529005144,3120562176,3525452322,4243451373,3952580503,3490318858,3539992576,4294967295,3847969627,3640655872,4294967295,3630326370,2952790016,3869483939,4260689140,3123917619,2415919104,3123917619,4260689140,4020084125,3640655872,3968239238,4260886519,3529005144,3120562176,3525452322,4243451373,3902446234,2735475724,2113929216,2600468480,3019898880,2583691264,2063597568,2063597568,2667577344,3714912256,4294781952,3640655872,4294781952,3547336960,1728053248,1191182336,2382495744,3902150144,4277545472,3580038656,3120562176,3559458048,4243531776,3986889472,3490382080,3539992576,4294781952,3882225408,3640655872,4294781952,3614249216,2634022912,2919235584,4294781952,3714912256,2667577344,2063597568,2063597568,2667577344,3714912256,4294781952,3640655872,4294781952,3547336960,2583691264,3120562176,4294781952,3547336960,2566914048,3547336960,4294781952,3640655872,3882225408,4294781952,2734686208,905969664, + 50331648,687865856,2164260864,4004986156,4293526023,4293132288,4292804608,4292739072,4190049031,3866102791,3816952331,3868467732,4003800346,4156500256,4275644710,4292683559,4292682277,4292679193,4292739073,4292739072,4292608000,4292411392,4292673536,4293661184,4174923273,3446360857,2164260864,1291845632,1191182336,2332033024,4073140736,4294119168,4293525504,4293066752,4293263360,4276230916,3260750080,1392508928,318767104,16777216,184549376,1275068416,3157340465,4274439878,3678486849,3120562176,3289452817,3848232799,4120681628,4291611852,3711251765,3456106496,4291611852,3799348597,2919235584,3288334336,4291611852,3461435729,2499805184,3410446151,4206212533,3778952766,3591574291,3388997632,3305111552,3170893824,3036676096,3372220416,4291611852,3427947090,2415919104,3019898880,4291611852,3427947090,1795162112,1795162112,3427947090,4291611852,3640655872,3760793897,4274439878,3678486849,3120562176,3289452817,3848232799,4120681628,4291611852,3795137845,3640655872,4291611852,3846785353,3640655872,4291611852,3478212945,2399141888,3073322799,4172394929,4121339558,3491832097,4121339558,4172394929,3542690089,3643353385,4274439878,3678486849,3120562176,3289452817,3848232799,4120681628,4291611852,3510188345,2785017856,3582069760,4090368512,3376612608,2920218624,2920218624,3393389824,4123791872,4037807872,3456106496,4294514688,3835038720,2231369728,1862270976,3191800832,4277278208,3696690944,3137339392,3323461888,3883600384,4140044544,4294514688,3813017088,3640655872,4294514688,3865118720,3640655872,4294514688,3496610048,2130706432,2399141888,3954183936,4123791872,3393389824,2920218624,2920218624,3393389824,4123791872,4071296768,3640655872,4294514688,3496610048,2466250752,3053453312,4294514688,3496610048,2466250752,3496610048,4294514688,3640655872,3865118720,4294514688,2734686208,905969664, + 201326592,1224736768,3023639812,4277017613,4293656576,4293263360,4292870144,4292804608,4292870144,4292871176,4292939794,4292939796,4292873232,4292871689,4292739587,4292804608,4292804608,4292673536,4292542464,4292345856,4292542464,4293133568,4157219336,3665638928,2651785731,1677721600,771751936,251658240,385875968,1526726656,3327729408,4294721792,4294119168,4293591040,4293197824,4293925893,3987551235,2248146944,872415232,134217728,385875968,1677721600,3630721128,4291546059,3813757265,3849088108,4189172145,4154893990,3881063508,4154762404,3781387107,3070230528,3629931612,4257728455,3661512254,3539992576,4291611852,3427947090,2231369728,2769885465,3934158462,4205949361,3847311697,3643419178,3729081669,4037585064,2902458368,3288334336,4291611852,3427947090,2415919104,3019898880,4291611852,3427947090,1795162112,1795162112,3427947090,4291611852,3640655872,3932250465,4291546059,3813757265,3849088108,4189172145,4154893990,3881063508,4154762404,3915275870,3640655872,4291611852,3846785353,3640655872,4291611852,3427947090,1929379840,1946157056,3309980234,4206541498,4274374085,4206541498,3427289160,2835349504,3731187045,4291546059,3813757265,3849088108,4189172145,4154893990,3881063508,4154762404,3865075808,3456106496,4294511104,4088530432,4294380032,3968205824,3968205824,4294380032,4038395392,3158180096,2533359616,3531015424,4260563200,3310682880,2583691264,3665954048,4294445568,3815047936,3867608064,4191881216,4157409024,3899130624,4157277952,3933602816,3640655872,4294511104,3864789504,3640655872,4294511104,3462923264,1778384896,1577058304,2957115648,4038395392,4294380032,3968205824,3968205824,4294380032,4038395392,3493396736,3472883712,4294511104,3462923264,2449473536,3036676096,4294511104,3462923264,2449473536,3462923264,4294511104,3640655872,3864789504,4294511104,2734686208,905969664, + 436207616,1795162112,3784914178,4294453760,4293985792,4293525504,4293263360,4293066752,4293001216,4292870144,4292870144,4292870144,4292804608,4292739072,4292608000,4292411392,4292411392,4292411392,4292804608,4276096257,4055439621,3462337541,2617967874,1778384896,1006632960,436207616,100663296,0,83886080,822083584,2332033024,4107292928,4294722560,4294251776,4293656576,4293856768,4276101633,3515097344,1342177280,234881024,436207616,1711276032,3817968017,4206673084,4223647679,4019952539,3613812326,3157077293,2869101315,3461238350,4037716650,2516582400,2365587456,3614865014,4104759721,3405774848,4291611852,3260503895,1560281088,1526726656,2703500324,3630786921,4036861341,4240490688,3867575942,2973514812,2231369728,2885681152,4291611852,3260503895,2080374784,2768240640,4291611852,3260503895,1493172224,1493172224,3260503895,4291611852,3372220416,3951922573,4206673084,4223647679,4019952539,3613812326,3157077293,2869101315,3461238350,4087982505,3405774848,4291611852,3662499149,3355443200,4291611852,3260503895,1358954496,872415232,1795162112,3224646708,4257662662,3224646708,2147483648,2147483648,3817968017,4206673084,4223647679,4019952539,3613812326,3157077293,2869101315,3461238350,4104628135,3590324224,4294508544,3815702528,3698589696,4073127936,4073127936,3598123008,2720661504,1543503872,1107296256,1929379840,3616538624,4057071616,2801795072,3870294016,4209246208,4226351104,4022140928,3615162368,3157852160,2869100544,3462266880,4090298368,3405774848,4294508544,3663659008,3355443200,4294508544,3261661184,1325400064,687865856,1476395008,2720661504,3598123008,4073127936,4073127936,3598123008,2720661504,2231369728,2902458368,4294508544,3261661184,2080374784,2768240640,4294508544,3261661184,2080374784,3261661184,4294508544,3355443200,3663659008,4294508544,2533359616,822083584, + 520093696,1962934272,4022817026,4294132225,4294521600,4294253056,4293920768,4293591296,4293197824,4293132288,4292935680,4292804608,4292804608,4292804608,4292935936,4293135104,4242084099,4072214529,3597801734,3023440387,2231369728,1577058304,956301312,452984832,117440512,16777216,0,0,0,352321536,1627389952,3530502912,4294929408,4294791936,4294592513,4158276864,3444975876,2535986688,1006632960,150994944,234881024,1006632960,1929379840,2449473536,2483027968,2164260864,1694498816,1258291200,1174405120,1610612736,1929379840,1459617792,1124073472,1660944384,2130706432,2264924160,2348810240,1744830464,687865856,436207616,1023410176,1694498816,2197815296,2348810240,1962934272,1224736768,989855744,1761607680,2348810240,1744830464,1140850688,1744830464,2348810240,1744830464,721420288,721420288,1744830464,2348810240,2197815296,2164260864,2449473536,2483027968,2164260864,1694498816,1258291200,1174405120,1610612736,2063597568,2248146944,2348810240,2164260864,2164260864,2348810240,1744830464,637534208,184549376,704643072,1728053248,2281701376,1728053248,939524096,1124073472,1929379840,2449473536,2483027968,2164260864,1694498816,1258291200,1174405120,1610612736,2483027968,3305111552,4294508544,3563126784,2449473536,2214592512,2147483648,1677721600,1023410176,402653184,234881024,788529152,1660944384,1996488704,1828716544,2030043136,2449473536,2483027968,2164260864,1694498816,1258291200,1174405120,1610612736,2063597568,2248146944,2348810240,2164260864,2164260864,2348810240,1744830464,637534208,150994944,402653184,1023410176,1677721600,2147483648,2147483648,1677721600,1023410176,905969664,1744830464,2348810240,1744830464,1140850688,1744830464,2348810240,1744830464,1140850688,1744830464,2348810240,2164260864,2164260864,2348810240,1677721600,503316480, + 318767104,1375731712,3059226113,3699846145,3869130506,4022230030,4141306627,4226171904,4260378112,4260178176,4259914240,4191428864,4089652224,3936955141,3648853506,3361147392,2887846915,2248146944,1694498816,1191182336,721420288,335544320,83886080,16777216,0,0,0,0,0,117440512,989855744,2585332736,4039860480,3784984577,3226543360,2382364672,1728053248,989855744,318767104,33554432,50331648,234881024,536870912,771751936,788529152,620756992,385875968,167772160,134217728,352321536,520093696,369098752,234881024,436207616,620756992,671088640,721420288,503316480,134217728,33554432,150994944,385875968,637534208,721420288,520093696,234881024,184549376,503316480,721420288,503316480,268435456,503316480,721420288,503316480,134217728,134217728,503316480,721420288,637534208,620756992,771751936,788529152,620756992,385875968,167772160,134217728,352321536,587202560,671088640,721420288,620756992,620756992,721420288,503316480,134217728,0,134217728,469762048,687865856,469762048,167772160,234881024,536870912,771751936,788529152,620756992,385875968,167772160,134217728,352321536,1258291200,2734686208,4294508544,3278503936,1476395008,754974720,620756992,385875968,150994944,33554432,16777216,150994944,436207616,570425344,503316480,587202560,771751936,788529152,620756992,385875968,167772160,134217728,352321536,587202560,671088640,721420288,620756992,620756992,721420288,503316480,134217728,0,33554432,150994944,385875968,620756992,620756992,385875968,150994944,150994944,503316480,721420288,503316480,268435456,503316480,721420288,503316480,268435456,503316480,721420288,620756992,620756992,721420288,503316480,134217728, + 67108864,503316480,1224736768,1744830464,1979711488,2181038080,2382364672,2533359616,2634022912,2634022912,2600468480,2466250752,2298478592,2046820352,1728053248,1409286144,1073741824,704643072,385875968,167772160,50331648,0,0,0,0,0,0,0,0,16777216,419430400,1342177280,1979711488,1862270976,1342177280,822083584,419430400,150994944,33554432,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,503316480,1677721600,2348810240,1744830464,637534208,67108864,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,50331648,234881024,419430400,536870912,637534208,738197504,822083584,855638016,872415232,838860800,788529152,687865856,570425344,419430400,251658240,117440512,33554432,0,0,0,0,0,0,0,0,0,0,0,0,67108864,335544320,536870912,469762048,234881024,50331648,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,134217728,503316480,721420288,503316480,134217728,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + ])); + return bmp; + } + + private var border:int = 5; + + public function Logo() { + graphics.beginFill(0xFF0000, 0); + graphics.drawRect(0, 0, image.width + border + border, image.height + border + border); + graphics.drawRect(border, border, image.width, image.height); + graphics.beginBitmapFill(image, new Matrix(1, 0, 0, 1, border, border), false, true); + graphics.drawRect(border, border, image.width, image.height); + + tabEnabled = false; + buttonMode = true; + useHandCursor = true; + + addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); + addEventListener(MouseEvent.CLICK, onClick); + addEventListener(MouseEvent.DOUBLE_CLICK, onDoubleClick); + addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove); + addEventListener(MouseEvent.MOUSE_OVER, onMouseMove); + addEventListener(MouseEvent.MOUSE_OUT, onMouseOut); + addEventListener(MouseEvent.MOUSE_WHEEL, onMouseWheel); + } + + private function onMouseDown(e:MouseEvent):void { + e.stopPropagation(); + } + + private function onClick(e:MouseEvent):void { + e.stopPropagation(); + try { + navigateToURL(new URLRequest("http://alternativaplatform.com"), "_blank"); + } catch (e:Error) { + } + } + + private function onDoubleClick(e:MouseEvent):void { + e.stopPropagation(); + } + + private static const normal:ColorTransform = new ColorTransform(); + private static const highlighted:ColorTransform = new ColorTransform(1.1, 1.1, 1.1, 1); + + private function onMouseMove(e:MouseEvent):void { + e.stopPropagation(); + transform.colorTransform = highlighted; + } + + private function onMouseOut(e:MouseEvent):void { + e.stopPropagation(); + transform.colorTransform = normal; + } + + private function onMouseWheel(e:MouseEvent):void { + e.stopPropagation(); + } + +} + +class Context3DViewProperties { + // Key - vertex program of object, value - program. + public var drawDistancePrograms:Dictionary = new Dictionary(); + public var drawColoredRectProgram:ShaderProgram; + public var drawRectGeometry:Geometry; + +} diff --git a/src/alternativa/engine3d/core/events/Event3D.as b/src/alternativa/engine3d/core/events/Event3D.as new file mode 100644 index 0000000..c0c32f5 --- /dev/null +++ b/src/alternativa/engine3d/core/events/Event3D.as @@ -0,0 +1,140 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core.events { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.*; + + import flash.events.Event; + + use namespace alternativa3d; + public class Event3D extends Event { + + /** + * Defines the value of the type property of a added event object. + * @eventType added + */ + public static const ADDED:String = "added3D"; + + /** + * Defines the value of the type property of a removed event object. + + * @eventType removed + */ + public static const REMOVED:String = "removed3D"; + + + /** + * This class should be used as base class for all events, which can have Object3D as an event target. + * @param type + * @param bubbles + */ + public function Event3D(type:String, bubbles:Boolean = true) { + super(type, bubbles); + _bubbles = bubbles; + } + + /** + * @private + */ + alternativa3d var _target:Object3D; + + /** + * @private + */ + alternativa3d var _currentTarget:Object3D; + + /** + * @private + */ + alternativa3d var _bubbles:Boolean; + + /** + * @private + */ + alternativa3d var _eventPhase:uint = 3; + + /** + * @private + */ + alternativa3d var stop:Boolean = false; + + /** + * @private + */ + alternativa3d var stopImmediate:Boolean = false; + + /** + * Indicates whether an event is a bubbling event. If the event can bubble, this value is true; otherwise it is false. + */ + override public function get bubbles():Boolean { + return _bubbles; + } + + /** + * The current phase in the event flow. + */ + override public function get eventPhase():uint { + return _eventPhase; + } + + /** + * The event target. This property contains the target node. + */ + override public function get target():Object { + return _target; + } + + /** + * The object that is actively processing the Event object with an event listener. + */ + override public function get currentTarget():Object { + return _currentTarget; + } + + /** + * Prevents processing of any event listeners in nodes subsequent to the current node in the event flow. + * Does not affect on receiving events in listeners of (currentTarget). + */ + override public function stopPropagation():void { + stop = true; + } + + /** + * Prevents processing of any event listeners in the current node and any subsequent nodes in the event flow. + */ + override public function stopImmediatePropagation():void { + stopImmediate = true; + } + + /** + * Duplicates an instance of an Event subclass. + * Returns a new Event3D object that is a copy of the original instance of the Event object. + * @return A new Event3D object that is identical to the original. + */ + override public function clone():Event { + var result:Event3D = new Event3D(type, _bubbles); + result._target = _target; + result._currentTarget = _currentTarget; + result._eventPhase = _eventPhase; + return result; + } + + /** + * Returns a string containing all the properties of the Event3D object. + * @return A string containing all the properties of the Event3D object + */ + override public function toString():String { + return formatToString("Event3D", "type", "bubbles", "eventPhase"); + } + + } +} diff --git a/src/alternativa/engine3d/core/events/MouseEvent3D.as b/src/alternativa/engine3d/core/events/MouseEvent3D.as new file mode 100644 index 0000000..f494884 --- /dev/null +++ b/src/alternativa/engine3d/core/events/MouseEvent3D.as @@ -0,0 +1,187 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.core.events { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.*; + import alternativa.engine3d.objects.Surface; + + import flash.events.Event; + + use namespace alternativa3d; + + /** + * + * Event MouseEvent3D dispatches by Object3D, in cases when MouseEvent dispatches by DisplayObject. + */ + public class MouseEvent3D extends Event3D { + + /** + * Defines the value of the type property of a click3D event object. + * @eventType click3D + */ + public static const CLICK:String = "click3D"; + + /** + * Defines the value of the type property of a doubleClick3D event object. + * @eventType doubleClick3D + */ + public static const DOUBLE_CLICK:String = "doubleClick3D"; + + /** + * Defines the value of the type property of a mouseDown3D event object. + * @eventType mouseDown3D + */ + public static const MOUSE_DOWN:String = "mouseDown3D"; + + /** + * Defines the value of the type property of a mouseUp3D event object. + * @eventType mouseUp3D + */ + public static const MOUSE_UP:String = "mouseUp3D"; + + /** + * Defines the value of the type property of a mouseOver3D event object. + * @eventType mouseOver3D + */ + public static const MOUSE_OVER:String = "mouseOver3D"; + + /** + * Defines the value of the type property of a mouseOut3D event object. + * @eventType mouseOut3D + */ + public static const MOUSE_OUT:String = "mouseOut3D"; + + /** + * Defines the value of the type property of a rollOver3D event object. + * @eventType rollOver3D + */ + public static const ROLL_OVER:String = "rollOver3D"; + + /** + * Defines the value of the type property of a rollOut3D event object. + * @eventType rollOut3D + */ + public static const ROLL_OUT:String = "rollOut3D"; + + /** + * Defines the value of the type property of a mouseMove3D event object. + * @eventType mouseMove3D + */ + public static const MOUSE_MOVE:String = "mouseMove3D"; + + /** + * Defines the value of the type property of a mouseWheel3D event object. + * @eventType mouseWheel3D + */ + public static const MOUSE_WHEEL:String = "mouseWheel3D"; + + /** + * On Windows or Linux, indicates whether the Ctrl key is active (true) or inactive (false). On Macintosh, indicates whether either the Control key or the Command key is activated. + */ + public var ctrlKey:Boolean; + /** + * Indicates whether the Alt key is active (true) or inactive (false). + */ + public var altKey:Boolean; + /** + * Indicates whether the Shift key is active (true) or inactive (false). + */ + public var shiftKey:Boolean; + /** + * Indicates whether the main mouse button is active (true) or inactive (false). + */ + public var buttonDown:Boolean; + /** + * Indicates how many lines should be scrolled for each unit the user rotates the mouse wheel. + */ + public var delta:int; + + /** + * A reference to an object that is related to the event. This property applies to the mouseOut, mouseOver, rollOut и rollOver. + * For example, when mouseOut occurs, relatedObject point to the object over which mouse cursor placed now. + */ + public var relatedObject:Object3D; + + /** + * X coordinate of the event at local target object's space. + */ + public var localX:Number; + + /** + * Y coordinate of the event at local target object's space. + */ + public var localY:Number; + + /** + * Z coordinate of the event at local target object's space. + */ + public var localZ:Number; + + /** + * @private + */ + alternativa3d var _surface:Surface; + + /** + * Creates a MouseEvent3D object. + * @param type Type. + * @param bubbles Indicates whether an event is a bubbling event. + * @param localY Y coordinate of the event at local target object's space. + * @param localX X coordinate of the event at local target object's space. + * @param localZ Z coordinate of the event at local target object's space. + * @param relatedObject Object3D, eelated to the MouseEvent3D. + * @param altKey Indicates whether the Alt key is active. + * @param ctrlKey Indicates whether the Control key is active. + * @param shiftKey Indicates whether the Shift key is active. + * @param buttonDown Indicates whether the main mouse button is active . + * @param delta Indicates how many lines should be scrolled for each unit the user rotates the mouse wheel. + */ + public function MouseEvent3D(type:String, bubbles:Boolean = true, localX:Number = NaN, localY:Number = NaN, localZ:Number = NaN, relatedObject:Object3D = null, ctrlKey:Boolean = false, altKey:Boolean = false, shiftKey:Boolean = false, buttonDown:Boolean = false, delta:int = 0) { + super(type, bubbles); + this.localX = localX; + this.localY = localY; + this.localZ = localZ; + this.relatedObject = relatedObject; + this.ctrlKey = ctrlKey; + this.altKey = altKey; + this.shiftKey = shiftKey; + this.buttonDown = buttonDown; + this.delta = delta; + } + + /** + * Surface on which event has been received. The object that owns the surface, can differs from the target event. + * + */ + public function get surface():Surface { + return _surface; + } + + /** + * Duplicates an instance of an Event subclass. + * Returns a new MouseEvent3D object that is a copy of the original instance of the Event object. + * @return A new MouseEvent3D object that is identical to the original. + */ + override public function clone():Event { + return new MouseEvent3D(type, _bubbles, localX, localY, localZ, relatedObject, ctrlKey, altKey, shiftKey, buttonDown, delta); + } + + /** + * Returns a string containing all the properties of the MouseEvent3D object. + * @return A string containing all the properties of the MouseEvent3D object + */ + override public function toString():String { + return formatToString("MouseEvent3D", "type", "bubbles", "eventPhase", "localX", "localY", "localZ", "relatedObject", "altKey", "ctrlKey", "shiftKey", "buttonDown", "delta"); + } + + } +} diff --git a/src/alternativa/engine3d/effects/AGALMiniAssembler.as b/src/alternativa/engine3d/effects/AGALMiniAssembler.as new file mode 100644 index 0000000..a867090 --- /dev/null +++ b/src/alternativa/engine3d/effects/AGALMiniAssembler.as @@ -0,0 +1,724 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.effects { + // =========================================================================== + // Imports + // --------------------------------------------------------------------------- + //import flash.display3D.*; + + import flash.utils.*; + + /** + * @private + */ + public class AGALMiniAssembler + { + // ====================================================================== + // Properties + // ---------------------------------------------------------------------- + // AGAL bytes and error buffer + private var _agalcode:ByteArray = null; + private var _error:String = ""; + + private var debugEnabled:Boolean = false; + + private static var initialized:Boolean = false; + + // ====================================================================== + // Getters + // ---------------------------------------------------------------------- + public function get error():String { return _error; } + public function get agalcode():ByteArray { return _agalcode; } + + // ====================================================================== + // Constructor + // ---------------------------------------------------------------------- + public function AGALMiniAssembler( debugging:Boolean = false ):void + { + debugEnabled = debugging; + if ( !initialized ) + init(); + } + // ====================================================================== + // Methods + // ---------------------------------------------------------------------- + public function assemble( mode:String, source:String, verbose:Boolean = false ):ByteArray + { + var start:uint = getTimer(); + + _agalcode = new ByteArray(); + _error = ""; + + var isFrag:Boolean = false; + + if ( mode == FRAGMENT ) + isFrag = true + else if ( mode != VERTEX ) + _error = 'ERROR: mode needs to be "' + FRAGMENT + '" or "' + VERTEX + '" but is "' + mode + '".'; + + agalcode.endian = Endian.LITTLE_ENDIAN; + agalcode.writeByte( 0xa0 ); // tag version + agalcode.writeUnsignedInt( 0x1 ); // AGAL version, big endian, bit pattern will be 0x01000000 + agalcode.writeByte( 0xa1 ); // tag program id + agalcode.writeByte( isFrag ? 1 : 0 ); // vertex or fragment + + var lines:Array = source.replace( /[\f\n\r\v]+/g, "\n" ).split( "\n" ); + var nest:int = 0; + var nops:int = 0; + var i:int; + var lng:int = lines.length; + + for ( i = 0; i < lng && _error == ""; i++ ) + { + var line:String = new String( lines[i] ); + + // remove comments + var startcomment:int = line.search( "//" ); + if ( startcomment != -1 ) + line = line.slice( 0, startcomment ); + + // grab options + var optsi:int = line.search( /<.*>/g ); + var opts:Array; + if ( optsi != -1 ) + { + opts = line.slice( optsi ).match( /([\w\.\-\+]+)/gi ); + line = line.slice( 0, optsi ); + } + + // find opcode + var opCode:Array = line.match( /^\w{3}/ig ); + var opFound:OpCode = OPMAP[ opCode[0] ]; + + // if debug is enabled, output the opcodes + if ( debugEnabled ) + trace( opFound ); + + if ( opFound == null ) + { + if ( line.length >= 3 ) + trace( "warning: bad line "+i+": "+lines[i] ); + continue; + } + + line = line.slice( line.search( opFound.name ) + opFound.name.length ); + + // nesting check + if ( opFound.flags & OP_DEC_NEST ) + { + nest--; + if ( nest < 0 ) + { + _error = "error: conditional closes without open."; + break; + } + } + if ( opFound.flags & OP_INC_NEST ) + { + nest++; + if ( nest > MAX_NESTING ) + { + _error = "error: nesting to deep, maximum allowed is "+MAX_NESTING+"."; + break; + } + } + if ( ( opFound.flags & OP_FRAG_ONLY ) && !isFrag ) + { + _error = "error: opcode is only allowed in fragment programs."; + break; + } + if ( verbose ) + trace( "emit opcode=" + opFound ); + + agalcode.writeUnsignedInt( opFound.emitCode ); + nops++; + + if ( nops > MAX_OPCODES ) + { + _error = "error: too many opcodes. maximum is "+MAX_OPCODES+"."; + break; + } + + // get operands, use regexp + var regs:Array = line.match( /vc\[([vof][actps]?)(\d*)?(\.[xyzw](\+\d{1,3})?)?\](\.[xyzw]{1,4})?|([vof][actps]?)(\d*)?(\.[xyzw]{1,4})?/gi ); + if ( regs.length != opFound.numRegister ) + { + _error = "error: wrong number of operands. found "+regs.length+" but expected "+opFound.numRegister+"."; + break; + } + + var badreg:Boolean = false; + var pad:uint = 64 + 64 + 32; + var regLength:uint = regs.length; + + for ( var j:int = 0; j < regLength; j++ ) + { + var isRelative:Boolean = false; + var relreg:Array = regs[ j ].match( /\[.*\]/ig ); + if ( relreg.length > 0 ) + { + regs[ j ] = regs[ j ].replace( relreg[ 0 ], "0" ); + + if ( verbose ) + trace( "IS REL" ); + isRelative = true; + } + + var res:Array = regs[j].match( /^\b[A-Za-z]{1,2}/ig ); + var regFound:Register = REGMAP[ res[ 0 ] ]; + + // if debug is enabled, output the registers + if ( debugEnabled ) + trace( regFound ); + + if ( regFound == null ) + { + _error = "error: could not parse operand "+j+" ("+regs[j]+")."; + badreg = true; + break; + } + + if ( isFrag ) + { + if ( !( regFound.flags & REG_FRAG ) ) + { + _error = "error: register operand "+j+" ("+regs[j]+") only allowed in vertex programs."; + badreg = true; + break; + } + if ( isRelative ) + { + _error = "error: register operand "+j+" ("+regs[j]+") relative adressing not allowed in fragment programs."; + badreg = true; + break; + } + } + else + { + if ( !( regFound.flags & REG_VERT ) ) + { + _error = "error: register operand "+j+" ("+regs[j]+") only allowed in fragment programs."; + badreg = true; + break; + } + } + + regs[j] = regs[j].slice( regs[j].search( regFound.name ) + regFound.name.length ); + //trace( "REGNUM: " +regs[j] ); + var idxmatch:Array = isRelative ? relreg[0].match( /\d+/ ) : regs[j].match( /\d+/ ); + var regidx:uint = 0; + + if ( idxmatch ) + regidx = uint( idxmatch[0] ); + + if ( regFound.range < regidx ) + { + _error = "error: register operand "+j+" ("+regs[j]+") index exceeds limit of "+(regFound.range+1)+"."; + badreg = true; + break; + } + + var regmask:uint = 0; + var maskmatch:Array = regs[j].match( /(\.[xyzw]{1,4})/ ); + var isDest:Boolean = ( j == 0 && !( opFound.flags & OP_NO_DEST ) ); + var isSampler:Boolean = ( j == 2 && ( opFound.flags & OP_SPECIAL_TEX ) ); + var reltype:uint = 0; + var relsel:uint = 0; + var reloffset:int = 0; + + if ( isDest && isRelative ) + { + _error = "error: relative can not be destination"; + badreg = true; + break; + } + + if ( maskmatch ) + { + regmask = 0; + var cv:uint; + var maskLength:uint = maskmatch[0].length; + for ( var k:int = 1; k < maskLength; k++ ) + { + cv = maskmatch[0].charCodeAt(k) - "x".charCodeAt(0); + if ( cv > 2 ) + cv = 3; + if ( isDest ) + regmask |= 1 << cv; + else + regmask |= cv << ( ( k - 1 ) << 1 ); + } + if ( !isDest ) + for ( ; k <= 4; k++ ) + regmask |= cv << ( ( k - 1 ) << 1 ) // repeat last + } + else + { + regmask = isDest ? 0xf : 0xe4; // id swizzle or mask + } + + if ( isRelative ) + { + var relname:Array = relreg[0].match( /[A-Za-z]{1,2}/ig ); + var regFoundRel:Register = REGMAP[ relname[0]]; + if ( regFoundRel == null ) + { + _error = "error: bad index register"; + badreg = true; + break; + } + reltype = regFoundRel.emitCode; + var selmatch:Array = relreg[0].match( /(\.[xyzw]{1,1})/ ); + if ( selmatch.length==0 ) + { + _error = "error: bad index register select"; + badreg = true; + break; + } + relsel = selmatch[0].charCodeAt(1) - "x".charCodeAt(0); + if ( relsel > 2 ) + relsel = 3; + var relofs:Array = relreg[0].match( /\+\d{1,3}/ig ); + if ( relofs.length > 0 ) + reloffset = relofs[0]; + if ( reloffset < 0 || reloffset > 255 ) + { + _error = "error: index offset "+reloffset+" out of bounds. [0..255]"; + badreg = true; + break; + } + if ( verbose ) + trace( "RELATIVE: type="+reltype+"=="+relname[0]+" sel="+relsel+"=="+selmatch[0]+" idx="+regidx+" offset="+reloffset ); + } + + if ( verbose ) + trace( " emit argcode="+regFound+"["+regidx+"]["+regmask+"]" ); + if ( isDest ) + { + agalcode.writeShort( regidx ); + agalcode.writeByte( regmask ); + agalcode.writeByte( regFound.emitCode ); + pad -= 32; + } else + { + if ( isSampler ) + { + if ( verbose ) + trace( " emit sampler" ); + var samplerbits:uint = 5; // type 5 + var optsLength:uint = opts.length; + var bias:Number = 0; + for ( k = 0; k = new Vector.(); + protected var positionKeys:Vector. = new Vector.(); + protected var directionKeys:Vector. = new Vector.(); + protected var scriptKeys:Vector. = new Vector.(); + protected var keysCount:int = 0; + + private static var randomNumbers:Vector.; + private static const randomNumbersCount:int = 1000; + + private static const vector:Vector3D = new Vector3D(); + + private var randomOffset:int; + private var randomCounter:int; + + private var _position:Vector3D = new Vector3D(0, 0, 0); + private var _direction:Vector3D = new Vector3D(0, 0, 1); + + public function ParticleEffect() { + if (randomNumbers == null) { + randomNumbers = new Vector.(); + for (var i:int = 0; i < randomNumbersCount; i++) randomNumbers[i] = Math.random(); + } + randomOffset = Math.random()*randomNumbersCount; + } + + public function get position():Vector3D { + return _position.clone(); + } + + public function set position(value:Vector3D):void { + _position.x = value.x; + _position.y = value.y; + _position.z = value.z; + _position.w = value.w; + if (system != null) setPositionKeys(system.getTime() - startTime); + } + + public function get direction():Vector3D { + return _direction.clone(); + } + + public function set direction(value:Vector3D):void { + _direction.x = value.x; + _direction.y = value.y; + _direction.z = value.z; + _direction.w = value.w; + if (system != null) setDirectionKeys(system.getTime() - startTime); + } + + public function stop():void { + var time:Number = system.getTime() - startTime; + for (var i:int = 0; i < keysCount; i++) { + if (time < timeKeys[i]) break; + } + keysCount = i; + } + + protected function get particleSystem():ParticleSystem { + return system; + } + + protected function get cameraTransform():Transform3D { + return system.cameraToLocalTransform; + } + + protected function random():Number { + var res:Number = randomNumbers[randomCounter]; randomCounter++; + if (randomCounter == randomNumbersCount) randomCounter = 0; + return res; + } + + protected function addKey(time:Number, script:Function):void { + timeKeys[keysCount] = time; + positionKeys[keysCount] = new Vector3D(); + directionKeys[keysCount] = new Vector3D(); + scriptKeys[keysCount] = script; + keysCount++; + } + + protected function setLife(time:Number):void { + lifeTime = time; + } + + alternativa3d function calculateAABB():void { + aabb.minX = boundBox.minX*scale + _position.x; + aabb.minY = boundBox.minY*scale + _position.y; + aabb.minZ = boundBox.minZ*scale + _position.z; + aabb.maxX = boundBox.maxX*scale + _position.x; + aabb.maxY = boundBox.maxY*scale + _position.y; + aabb.maxZ = boundBox.maxZ*scale + _position.z; + } + + alternativa3d function setPositionKeys(time:Number):void { + for (var i:int = 0; i < keysCount; i++) { + if (time <= timeKeys[i]) { + var pos:Vector3D = positionKeys[i]; + pos.x = _position.x; + pos.y = _position.y; + pos.z = _position.z; + } + } + } + + alternativa3d function setDirectionKeys(time:Number):void { + vector.x = _direction.x; + vector.y = _direction.y; + vector.z = _direction.z; + vector.normalize(); + for (var i:int = 0; i < keysCount; i++) { + if (time <= timeKeys[i]) { + var dir:Vector3D = directionKeys[i]; + dir.x = vector.x; + dir.y = vector.y; + dir.z = vector.z; + } + } + } + + alternativa3d function calculate(time:Number):Boolean { + randomCounter = randomOffset; + for (var i:int = 0; i < keysCount; i++) { + var keyTime:Number = timeKeys[i]; + if (time >= keyTime) { + keyPosition = positionKeys[i]; + keyDirection = directionKeys[i]; + var script:Function = scriptKeys[i]; + script.call(this, keyTime, time - keyTime); + } else break; + } + return i < keysCount || particleList != null; + } + + } +} diff --git a/src/alternativa/engine3d/effects/ParticlePrototype.as b/src/alternativa/engine3d/effects/ParticlePrototype.as new file mode 100644 index 0000000..bf5f1d8 --- /dev/null +++ b/src/alternativa/engine3d/effects/ParticlePrototype.as @@ -0,0 +1,139 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.effects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Transform3D; + + import flash.geom.Vector3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class ParticlePrototype { + + // Atlas + public var atlas:TextureAtlas; + + // Blend + private var blendSource:String; + private var blendDestination:String; + + // If true, then play animation + private var animated:Boolean; + + // Size + private var width:Number; + private var height:Number; + + // Key frames of animation. + private var timeKeys:Vector. = new Vector.(); + private var rotationKeys:Vector. = new Vector.(); + private var scaleXKeys:Vector. = new Vector.(); + private var scaleYKeys:Vector. = new Vector.(); + private var redKeys:Vector. = new Vector.(); + private var greenKeys:Vector. = new Vector.(); + private var blueKeys:Vector. = new Vector.(); + private var alphaKeys:Vector. = new Vector.(); + private var keysCount:int = 0; + + public function ParticlePrototype(width:Number, height:Number, atlas:TextureAtlas, animated:Boolean = false, blendSource:String = "sourceAlpha", blendDestination:String = "oneMinusSourceAlpha") { + this.width = width; + this.height = height; + this.atlas = atlas; + this.animated = animated; + this.blendSource = blendSource; + this.blendDestination = blendDestination; + } + + public function addKey(time:Number, rotation:Number = 0, scaleX:Number = 1, scaleY:Number = 1, red:Number = 1, green:Number = 1, blue:Number = 1, alpha:Number = 1):void { + var lastIndex:int = keysCount - 1; + if (keysCount > 0 && time <= timeKeys[lastIndex]) throw new Error("Keys must be successively."); + timeKeys[keysCount] = time; + rotationKeys[keysCount] = rotation; + scaleXKeys[keysCount] = scaleX; + scaleYKeys[keysCount] = scaleY; + redKeys[keysCount] = red; + greenKeys[keysCount] = green; + blueKeys[keysCount] = blue; + alphaKeys[keysCount] = alpha; + keysCount++; + } + + public function createParticle(effect:ParticleEffect, time:Number, position:Vector3D, rotation:Number = 0, scaleX:Number = 1, scaleY:Number = 1, alpha:Number = 1, firstFrame:int = 0):void { + var b:int = keysCount - 1; + if (atlas.diffuse._texture != null && keysCount > 1 && time >= timeKeys[0] && time < timeKeys[b]) { + + for (b = 1; b < keysCount; b++) { + if (time < timeKeys[b]) { + var systemScale:Number = effect.system.scale; + var effectScale:Number = effect.scale; + var transform:Transform3D = effect.system.localToCameraTransform; + var wind:Vector3D = effect.system.wind; + var gravity:Vector3D = effect.system.gravity; + // Interpolation + var a:int = b - 1; + var t:Number = (time - timeKeys[a])/(timeKeys[b] - timeKeys[a]); + // Frame calculation + var pos:int = firstFrame + (animated ? time*atlas.fps : 0); + if (atlas.loop) { + pos = pos%atlas.rangeLength; + if (pos < 0) pos += atlas.rangeLength; + } else { + if (pos < 0) pos = 0; + if (pos >= atlas.rangeLength) pos = atlas.rangeLength - 1; + } + pos += atlas.rangeBegin; + var col:int = pos%atlas.columnsCount; + var row:int = pos/atlas.columnsCount; + // Particle creation + var particle:Particle = Particle.create(); + particle.diffuse = atlas.diffuse._texture; + particle.opacity = (atlas.opacity != null) ? atlas.opacity._texture : null; + particle.blendSource = blendSource; + particle.blendDestination = blendDestination; + var cx:Number = effect.keyPosition.x + position.x*effectScale; + var cy:Number = effect.keyPosition.y + position.y*effectScale; + var cz:Number = effect.keyPosition.z + position.z*effectScale; + particle.x = cx*transform.a + cy*transform.b + cz*transform.c + transform.d; + particle.y = cx*transform.e + cy*transform.f + cz*transform.g + transform.h; + particle.z = cx*transform.i + cy*transform.j + cz*transform.k + transform.l; + var rot:Number = rotationKeys[a] + (rotationKeys[b] - rotationKeys[a])*t; + particle.rotation = (scaleX*scaleY > 0) ? (rotation + rot) : (rotation - rot); + particle.width = systemScale*effectScale*scaleX*width*(scaleXKeys[a] + (scaleXKeys[b] - scaleXKeys[a])*t); + particle.height = systemScale*effectScale*scaleY*height*(scaleYKeys[a] + (scaleYKeys[b] - scaleYKeys[a])*t); + particle.originX = atlas.originX; + particle.originY = atlas.originY; + particle.uvScaleX = 1/atlas.columnsCount; + particle.uvScaleY = 1/atlas.rowsCount; + particle.uvOffsetX = col/atlas.columnsCount; + particle.uvOffsetY = row/atlas.rowsCount; + particle.red = redKeys[a] + (redKeys[b] - redKeys[a])*t; + particle.green = greenKeys[a] + (greenKeys[b] - greenKeys[a])*t; + particle.blue = blueKeys[a] + (blueKeys[b] - blueKeys[a])*t; + particle.alpha = alpha*(alphaKeys[a] + (alphaKeys[b] - alphaKeys[a])*t); + particle.next = effect.particleList; + effect.particleList = particle; + break; + } + } + } + } + + public function get lifeTime():Number { + var lastIndex:int = keysCount - 1; + return timeKeys[lastIndex]; + } + + } +} diff --git a/src/alternativa/engine3d/effects/ParticleSystem.as b/src/alternativa/engine3d/effects/ParticleSystem.as new file mode 100644 index 0000000..c7099a5 --- /dev/null +++ b/src/alternativa/engine3d/effects/ParticleSystem.as @@ -0,0 +1,481 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.effects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Debug; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.materials.compiler.Procedure; + + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DProgramType; + import flash.display3D.Context3DTriangleFace; + import flash.display3D.Context3DVertexBufferFormat; + import flash.display3D.IndexBuffer3D; + import flash.display3D.Program3D; + import flash.display3D.VertexBuffer3D; + import flash.display3D.textures.TextureBase; + import flash.geom.Vector3D; + import flash.utils.ByteArray; + import flash.utils.getTimer; + + use namespace alternativa3d; + + /** + * @private + */ + public class ParticleSystem extends Object3D { + + static private const limit:int = 31; + static private var vertexBuffer:VertexBuffer3D; + static private var indexBuffer:IndexBuffer3D; + static private var diffuseProgram:Program3D; + static private var opacityProgram:Program3D; + static private var diffuseBlendProgram:Program3D; + static private var opacityBlendProgram:Program3D; + + public var resolveByAABB:Boolean = true; + + public var gravity:Vector3D = new Vector3D(0, 0, -1); + public var wind:Vector3D = new Vector3D(); + + public var fogColor:int = 0; + public var fogMaxDensity:Number = 0; + public var fogNear:Number = 0; + public var fogFar:Number = 0; + + alternativa3d var scale:Number = 1; + + alternativa3d var effectList:ParticleEffect; + + private var drawUnit:DrawUnit = null; + private var diffuse:TextureBase = null; + private var opacity:TextureBase = null; + private var blendSource:String = null; + private var blendDestination:String = null; + private var counter:int; + + private var za:Number; + private var zb:Number; + private var fake:Vector. = new Vector.(); + private var fakeCounter:int = 0; + + public function ParticleSystem() { + super(); + } + + private var pause:Boolean = false; + private var stopTime:Number; + private var subtractiveTime:Number = 0; + + public function stop():void { + if (!pause) { + stopTime = getTimer()*0.001; + pause = true; + } + } + + public function play():void { + if (pause) { + subtractiveTime += getTimer()*0.001 - stopTime; + pause = false; + } + } + + public function prevFrame():void { + stopTime -= 0.001; + } + + public function nextFrame():void { + stopTime += 0.001; + } + + public function addEffect(effect:ParticleEffect):ParticleEffect { + // Checking on belonging + if (effect.system != null) throw new Error("Cannot add the same effect twice."); + // Set parameters + effect.startTime = getTime(); + effect.system = this; + effect.setPositionKeys(0); + effect.setDirectionKeys(0); + // Add + effect.nextInSystem = effectList; + effectList = effect; + return effect; + } + + public function getEffectByName(name:String):ParticleEffect { + for (var effect:ParticleEffect = effectList; effect != null; effect = effect.nextInSystem) { + if (effect.name == name) return effect; + } + return null; + } + + alternativa3d function getTime():Number { + return pause ? (stopTime - subtractiveTime) : (getTimer()*0.001 - subtractiveTime); + } + + override alternativa3d function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + // Create geometry and program + if (vertexBuffer == null) createAndUpload(camera.context3D); + // Average size + scale = Math.sqrt(localToCameraTransform.a*localToCameraTransform.a + localToCameraTransform.e*localToCameraTransform.e + localToCameraTransform.i*localToCameraTransform.i); + scale += Math.sqrt(localToCameraTransform.b*localToCameraTransform.b + localToCameraTransform.f*localToCameraTransform.f + localToCameraTransform.j*localToCameraTransform.j); + scale += Math.sqrt(localToCameraTransform.c*localToCameraTransform.c + localToCameraTransform.g*localToCameraTransform.g + localToCameraTransform.k*localToCameraTransform.k); + scale /= 3; + // TODO: add rotation on slope of the Z-axis in local space of camera + // Calculate frustrum + camera.calculateFrustum(cameraToLocalTransform); + // Loop items + var visibleEffectList:ParticleEffect; + var conflictAnyway:Boolean = false; + var time:Number = getTime(); + for (var effect:ParticleEffect = effectList, prev:ParticleEffect = null; effect != null;) { + // Check if actual + var effectTime:Number = time - effect.startTime; + if (effectTime <= effect.lifeTime) { + // Check bounds + var culling:int = 63; + if (effect.boundBox != null) { + effect.calculateAABB(); + culling = effect.aabb.checkFrustumCulling(camera.frustum, 63); + } + if (culling >= 0) { + // Gather the particles + if (effect.calculate(effectTime)) { + // Add + if (effect.particleList != null) { + effect.next = visibleEffectList; + visibleEffectList = effect; + conflictAnyway ||= effect.boundBox == null; + } + // Go to next effect + prev = effect; + effect = effect.nextInSystem; + } else { + // Removing + if (prev != null) { + prev.nextInSystem = effect.nextInSystem; + effect = prev.nextInSystem; + } else { + effectList = effect.nextInSystem; + effect = effectList; + } + } + } else { + // Go to next effect + prev = effect; + effect = effect.nextInSystem; + } + } else { + // Removing + if (prev != null) { + prev.nextInSystem = effect.nextInSystem; + effect = prev.nextInSystem; + } else { + effectList = effect.nextInSystem; + effect = effectList; + } + } + } + // Gather draws + if (visibleEffectList != null) { + if (visibleEffectList.next != null) { + /*if (resolveByAABB && !conflictAnyway) { + drawAABBEffects(camera, visibleEffectList); + } else {*/ + drawConflictEffects(camera, visibleEffectList); + //} + } else { + drawParticleList(camera, visibleEffectList.particleList); + visibleEffectList.particleList = null; + if (camera.debug && visibleEffectList.boundBox != null && (camera.checkInDebug(this) & Debug.BOUNDS)) Debug.drawBoundBox(camera, visibleEffectList.aabb, localToCameraTransform); + } + // Reset + flush(camera); + drawUnit = null; + diffuse = null; + opacity = null; + blendSource = null; + blendDestination = null; + fakeCounter = 0; + } + } + + private function createAndUpload(context:Context3D):void { + var vertices:Vector. = new Vector.(); + var indices:Vector. = new Vector.(); + for (var i:int = 0; i < limit; i++) { + vertices.push(0,0,0, 0,0,i*4, 0,1,0, 0,1,i*4, 1,1,0, 1,1,i*4, 1,0,0, 1,0,i*4); + indices.push(i*4, i*4 + 1, i*4 + 3, i*4 + 2, i*4 + 3, i*4 + 1); + } + vertexBuffer = context.createVertexBuffer(limit*4, 6); + vertexBuffer.uploadFromVector(vertices, 0, limit*4); + indexBuffer = context.createIndexBuffer(limit*6); + indexBuffer.uploadFromVector(indices, 0, limit*6); + var vertexProgram:Array = [ + // Pivot + "mov t2, c[a1.z]", // originX, originY, width, height + "sub t0.z, a0.x, t2.x", + "sub t0.w, a0.y, t2.y", + // Width and height + "mul t0.z, t0.z, t2.z", + "mul t0.w, t0.w, t2.w", + // Rotation + "mov t2, c[a1.z+1]", // x, y, z, rotation + "mov t1.z, t2.w", + "sin t1.x, t1.z", // sin + "cos t1.y, t1.z", // cos + "mul t1.z, t0.z, t1.y", // x*cos + "mul t1.w, t0.w, t1.x", // y*sin + "sub t0.x, t1.z, t1.w", // X + "mul t1.z, t0.z, t1.x", // x*sin + "mul t1.w, t0.w, t1.y", // y*cos + "add t0.y, t1.z, t1.w", // Y + // Translation + "add t0.x, t0.x, t2.x", + "add t0.y, t0.y, t2.y", + "add t0.z, a0.z, t2.z", + "mov t0.w, a0.w", + // Projection + "dp4 o0.x, t0, c124", + "dp4 o0.y, t0, c125", + "dp4 o0.z, t0, c126", + "dp4 o0.w, t0, c127", + // UV correction and passing out + "mov t2, c[a1.z+2]", // uvScaleX, uvScaleY, uvOffsetX, uvOffsetY + "mul t1.x, a1.x, t2.x", + "mul t1.y, a1.y, t2.y", + "add t1.x, t1.x, t2.z", + "add t1.y, t1.y, t2.w", + "mov v0, t1", + // Passing color + "mov v1, c[a1.z+3]", // red, green, blue, alpha + // Passing coordinates in the camera space + "mov v2, t0", + ]; + var fragmentDiffuseProgram:Array = [ + "tex t0, v0, s0 <2d,clamp,linear,miplinear>", + "mul t0, t0, v1", + // Fog + "sub t1.w, v2.z, c1.x", + "div t1.w, t1.w, c1.y", + "max t1.w, t1.w, c1.z", + "min t1.w, t1.w, c0.w", + "sub t1.xyz, c0.xyz, t0.xyz", + "mul t1.xyz, t1.xyz, t1.w", + "add t0.xyz, t0.xyz, t1.xyz", + "mov o0, t0", + ]; + var fragmentOpacityProgram:Array = [ + "tex t0, v0, s0 <2d,clamp,linear,miplinear>", + "tex t1, v0, s1 <2d,clamp,linear,miplinear>", + "mov t0.w, t1.x", + "mul t0, t0, v1", + // Fog + "sub t1.w, v2.z, c1.x", + "div t1.w, t1.w, c1.y", + "max t1.w, t1.w, c1.z", + "min t1.w, t1.w, c0.w", + "sub t1.xyz, c0.xyz, t0.xyz", + "mul t1.xyz, t1.xyz, t1.w", + "add t0.xyz, t0.xyz, t1.xyz", + "mov o0, t0", + ]; + var fragmentDiffuseBlendProgram:Array = [ + "tex t0, v0, s0 <2d,clamp,linear,miplinear>", + "mul t0, t0, v1", + // Fog + "sub t1.w, v2.z, c1.x", + "div t1.w, t1.w, c1.y", + "max t1.w, t1.w, c1.z", + "min t1.w, t1.w, c0.w", + "sub t1.w, c1.w, t1.w", + "mul t0.w, t0.w, t1.w", + "mov o0, t0", + ]; + var fragmentOpacityBlendProgram:Array = [ + "tex t0, v0, s0 <2d,clamp,linear,miplinear>", + "tex t1, v0, s1 <2d,clamp,linear,miplinear>", + "mov t0.w, t1.x", + "mul t0, t0, v1", + // Fog + "sub t1.w, v2.z, c1.x", + "div t1.w, t1.w, c1.y", + "max t1.w, t1.w, c1.z", + "min t1.w, t1.w, c0.w", + "sub t1.w, c1.w, t1.w", + "mul t0.w, t0.w, t1.w", + "mov o0, t0", + ]; + diffuseProgram = context.createProgram(); + opacityProgram = context.createProgram(); + diffuseBlendProgram = context.createProgram(); + opacityBlendProgram = context.createProgram(); + var compiledVertexProgram:ByteArray = compileProgram(Context3DProgramType.VERTEX, vertexProgram); + diffuseProgram.upload(compiledVertexProgram, compileProgram(Context3DProgramType.FRAGMENT, fragmentDiffuseProgram)); + opacityProgram.upload(compiledVertexProgram, compileProgram(Context3DProgramType.FRAGMENT, fragmentOpacityProgram)); + diffuseBlendProgram.upload(compiledVertexProgram, compileProgram(Context3DProgramType.FRAGMENT, fragmentDiffuseBlendProgram)); + opacityBlendProgram.upload(compiledVertexProgram, compileProgram(Context3DProgramType.FRAGMENT, fragmentOpacityBlendProgram)); + } + + private function compileProgram(mode:String, program:Array):ByteArray { + /*var string:String = ""; + var length:int = program.length; + for (var i:int = 0; i < length; i++) { + var line:String = program[i]; + string += line + ((i < length - 1) ? " \n" : ""); + }*/ + var proc:Procedure = new Procedure(program); + return proc.getByteCode(mode); + } + + private function flush(camera:Camera3D):void { + if (fakeCounter == fake.length) fake[fakeCounter] = new Object3D(); + var object:Object3D = fake[fakeCounter]; + fakeCounter++; + object.localToCameraTransform.l = (za + zb)/2; + // Fill + drawUnit.object = object; + drawUnit.numTriangles = counter << 1; + if (blendDestination == Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA) { + drawUnit.program = (opacity != null) ? opacityProgram : diffuseProgram; + } else { + drawUnit.program = (opacity != null) ? opacityBlendProgram : diffuseBlendProgram; + } + // Set streams + drawUnit.setVertexBufferAt(0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3); + drawUnit.setVertexBufferAt(1, vertexBuffer, 3, Context3DVertexBufferFormat.FLOAT_3); + // Set constants + drawUnit.setProjectionConstants(camera, 124); + drawUnit.setFragmentConstantsFromNumbers(0, ((fogColor >> 16) & 0xFF)/0xFF, ((fogColor >> 8) & 0xFF)/0xFF, (fogColor & 0xFF)/0xFF, fogMaxDensity); + drawUnit.setFragmentConstantsFromNumbers(1, fogNear, fogFar - fogNear, 0, 1); + // Set textures + drawUnit.setTextureAt(0, diffuse); + if (opacity != null) drawUnit.setTextureAt(1, opacity); + // Set blending + drawUnit.blendSource = blendSource; + drawUnit.blendDestination = blendDestination; + drawUnit.culling = Context3DTriangleFace.NONE; + // Send to render + camera.renderer.addDrawUnit(drawUnit, Renderer.TRANSPARENT_SORT); + } + + private function drawParticleList(camera:Camera3D, list:Particle):void { + // Sorting + if (list.next != null) list = sortParticleList(list); + // Gather draws + var last:Particle; + for (var particle:Particle = list; particle != null; particle = particle.next) { + if (counter >= limit || particle.diffuse != diffuse || particle.opacity != opacity || particle.blendSource != blendSource || particle.blendDestination != blendDestination) { + if (drawUnit != null) flush(camera); + drawUnit = camera.renderer.createDrawUnit(null, null, indexBuffer, 0, 0); + diffuse = particle.diffuse; + opacity = particle.opacity; + blendSource = particle.blendSource; + blendDestination = particle.blendDestination; + counter = 0; + za = particle.z; + } + // Write constants + var offset:int = counter << 2; + drawUnit.setVertexConstantsFromNumbers(offset++, particle.originX, particle.originY, particle.width, particle.height); + drawUnit.setVertexConstantsFromNumbers(offset++, particle.x, particle.y, particle.z, particle.rotation); + drawUnit.setVertexConstantsFromNumbers(offset++, particle.uvScaleX, particle.uvScaleY, particle.uvOffsetX, particle.uvOffsetY); + drawUnit.setVertexConstantsFromNumbers(offset++, particle.red, particle.green, particle.blue, particle.alpha); + counter++; + zb = particle.z; + last = particle; + } + // Send to the collector + last.next = Particle.collector; + Particle.collector = list; + } + + private function sortParticleList(list:Particle):Particle { + var left:Particle = list; + var right:Particle = list.next; + while (right != null && right.next != null) { + list = list.next; + right = right.next.next; + } + right = list.next; + list.next = null; + if (left.next != null) { + left = sortParticleList(left); + } + if (right.next != null) { + right = sortParticleList(right); + } + var flag:Boolean = left.z > right.z; + if (flag) { + list = left; + left = left.next; + } else { + list = right; + right = right.next; + } + var last:Particle = list; + while (true) { + if (left == null) { + last.next = right; + return list; + } else if (right == null) { + last.next = left; + return list; + } + if (flag) { + if (left.z > right.z) { + last = left; + left = left.next; + } else { + last.next = right; + last = right; + right = right.next; + flag = false; + } + } else { + if (right.z > left.z) { + last = right; + right = right.next; + } else { + last.next = left; + last = left; + left = left.next; + flag = true; + } + } + } + return null; + } + + private function drawConflictEffects(camera:Camera3D, effectList:ParticleEffect):void { + var particleList:Particle; + for (var effect:ParticleEffect = effectList; effect != null; effect = next) { + var next:ParticleEffect = effect.next; + effect.next = null; + var last:Particle = effect.particleList; + while (last.next != null) last = last.next; + last.next = particleList; + particleList = effect.particleList; + effect.particleList = null; + if (camera.debug && effect.boundBox != null && (camera.checkInDebug(this) & Debug.BOUNDS)) Debug.drawBoundBox(camera, effect.aabb, localToCameraTransform, 0xFF0000); + } + drawParticleList(camera, particleList); + } + + } +} diff --git a/src/alternativa/engine3d/effects/TextureAtlas.as b/src/alternativa/engine3d/effects/TextureAtlas.as new file mode 100644 index 0000000..2c2283a --- /dev/null +++ b/src/alternativa/engine3d/effects/TextureAtlas.as @@ -0,0 +1,45 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.effects { + + import alternativa.engine3d.resources.TextureResource; + + /** + * @private + */ + public class TextureAtlas { + + public var diffuse:TextureResource; + public var opacity:TextureResource; + public var columnsCount:int; + public var rowsCount:int; + public var rangeBegin:int; + public var rangeLength:int; + public var fps:int; + public var loop:Boolean; + public var originX:Number; + public var originY:Number; + + public function TextureAtlas(diffuse:TextureResource, opacity:TextureResource = null, columnsCount:int = 1, rowsCount:int = 1, rangeBegin:int = 0, rangeLength:int = 1, fps:int = 30, loop:Boolean = false, originX:Number = 0.5, originY:Number = 0.5) { + this.diffuse = diffuse; + this.opacity = opacity; + this.columnsCount = columnsCount; + this.rowsCount = rowsCount; + this.rangeBegin = rangeBegin; + this.rangeLength = rangeLength; + this.fps = fps; + this.loop = loop; + this.originX = originX; + this.originY = originY; + } + + } +} diff --git a/src/alternativa/engine3d/lights/AmbientLight.as b/src/alternativa/engine3d/lights/AmbientLight.as new file mode 100644 index 0000000..fdfb522 --- /dev/null +++ b/src/alternativa/engine3d/lights/AmbientLight.as @@ -0,0 +1,64 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.lights { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + + use namespace alternativa3d; + + /** + * An ambient light source represents a fixed-intensity and fixed-color light source + * that affects all objects in the scene equally. Upon rendering, all objects in + * the scene are brightened with the specified intensity and color. + * This type of light source is mainly used to provide the scene with a basic view of the different objects in it. + * + * This description taken from http://en.wikipedia.org/wiki/Shading#Ambient_lighting + */ + public class AmbientLight extends Light3D { + + /** + * Creates a AmbientLight object. + * @param color Light color. + */ + public function AmbientLight(color:uint) { + this.color = color; + } + + /** + * Does not do anything. + * + */ + override public function calculateBoundBox():void { + } + + /** + * @private + */ + override alternativa3d function calculateVisibility(camera:Camera3D):void { + camera.ambient[0] += ((color >> 16) & 0xFF)*intensity/255; + camera.ambient[1] += ((color >> 8) & 0xFF)*intensity/255; + camera.ambient[2] += (color & 0xFF)*intensity/255; + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:AmbientLight = new AmbientLight(color); + res.clonePropertiesFrom(this); + return res; + } + + } +} diff --git a/src/alternativa/engine3d/lights/DirectionalLight.as b/src/alternativa/engine3d/lights/DirectionalLight.as new file mode 100644 index 0000000..583814f --- /dev/null +++ b/src/alternativa/engine3d/lights/DirectionalLight.as @@ -0,0 +1,67 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.lights { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + + use namespace alternativa3d; + + /** + * A directional light source illuminates all objects equally from a given direction, + * like an area light of infinite size and infinite distance from the scene; + * there is shading, but cannot be any distance falloff. + * + * This description taken from http://en.wikipedia.org/wiki/Shading#Directional_lighting + * + * Lightning direction defines by z-axis of DirectionalLight. + * You can use lookAt() to make DirectionalLight point at given coordinates. + */ + public class DirectionalLight extends Light3D { + + /** + * Creates a new instance. + * @param color Color of light source. + */ + public function DirectionalLight(color:uint) { + this.color = color; + } + + /** + * Sets direction of DirectionalLight to given coordinates. + */ + public function lookAt(x:Number, y:Number, z:Number):void { + var dx:Number = x - this.x; + var dy:Number = y - this.y; + var dz:Number = z - this.z; + rotationX = Math.atan2(dz, Math.sqrt(dx*dx + dy*dy)) - Math.PI/2; + rotationY = 0; + rotationZ = -Math.atan2(dx, dy); + } + + /** + * Does not do anything. + */ + override public function calculateBoundBox():void { + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:DirectionalLight = new DirectionalLight(color); + res.clonePropertiesFrom(this); + return res; + } + + } +} diff --git a/src/alternativa/engine3d/lights/OmniLight.as b/src/alternativa/engine3d/lights/OmniLight.as new file mode 100644 index 0000000..2009edc --- /dev/null +++ b/src/alternativa/engine3d/lights/OmniLight.as @@ -0,0 +1,206 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.lights { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + + use namespace alternativa3d; + + /** + * OmniLight is an attenuated light source placed at one point and spreads outward in all directions. + * + */ + public class OmniLight extends Light3D { + + /** + * Distance from which falloff starts. + */ + public var attenuationBegin:Number; + + /** + * Distance from at which falloff is complete. + */ + public var attenuationEnd:Number; + + /** + * Creates a OmniLight object. + * @param color Light color. + * @param attenuationBegin Distance from which falloff starts. + * @param attenuationEnd Distance from at which falloff is complete. + */ + public function OmniLight(color:uint, attenuationBegin:Number, attenuationEnd:Number) { + this.color = color; + this.attenuationBegin = attenuationBegin; + this.attenuationEnd = attenuationEnd; + calculateBoundBox(); + } + + /** + * @private + */ + override alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + if (transform != null) { + + } else { + if (-attenuationEnd < boundBox.minX) boundBox.minX = -attenuationEnd; + if (attenuationEnd > boundBox.maxX) boundBox.maxX = attenuationEnd; + if (-attenuationEnd < boundBox.minY) boundBox.minY = -attenuationEnd; + if (attenuationEnd > boundBox.maxY) boundBox.maxY = attenuationEnd; + if (-attenuationEnd < boundBox.minZ) boundBox.minZ = -attenuationEnd; + if (attenuationEnd > boundBox.maxZ) boundBox.maxZ = attenuationEnd; + } + } + + /** + * @private + */ + override alternativa3d function checkBound(targetObject:Object3D):Boolean { + var rScale:Number = Math.sqrt(lightToObjectTransform.a*lightToObjectTransform.a + lightToObjectTransform.e*lightToObjectTransform.e + lightToObjectTransform.i*lightToObjectTransform.i); + rScale += Math.sqrt(lightToObjectTransform.b*lightToObjectTransform.b + lightToObjectTransform.f*lightToObjectTransform.f + lightToObjectTransform.j*lightToObjectTransform.j); + rScale += Math.sqrt(lightToObjectTransform.c*lightToObjectTransform.c + lightToObjectTransform.g*lightToObjectTransform.g + lightToObjectTransform.k*lightToObjectTransform.k); + rScale /= 3; + rScale *= attenuationEnd; + rScale *= rScale; + var len:Number = 0; + var bb:BoundBox = targetObject.boundBox; + var minX:Number = bb.minX; + var minY:Number = bb.minY; + var minZ:Number = bb.minZ; + var maxX:Number = bb.maxX; + var px:Number = lightToObjectTransform.d; + var py:Number = lightToObjectTransform.h; + var pz:Number = lightToObjectTransform.l; + + var maxY:Number = bb.maxY; + var maxZ:Number = bb.maxZ; + if (px < minX) { + if (py < minY) { + if (pz < minZ) { + len = (minX - px)*(minX - px) + (minY - py)*(minY - py) + (minZ - pz)*(minZ - pz); + return len < rScale; + } else if (pz < maxZ) { + len = (minX - px)*(minX - px) + (minY - py)*(minY - py); + return len < rScale; + } else if (pz > maxZ) { + len = (minX - px)*(minX - px) + (minY - py)*(minY - py) + (maxZ - pz)*(maxZ - pz); + return len < rScale; + } + } else if (py < maxY) { + if (pz < minZ) { + len = (minX - px)*(minX - px) + (minZ - pz)*(minZ - pz); + return len < rScale; + } else if (pz < maxZ) { + len = (minX - px)*(minX - px); + return len < rScale; + } else if (pz > maxZ) { + len = (minX - px)*(minX - px) + (maxZ - pz)*(maxZ - pz); + return len < rScale; + } + } else if (py > maxY) { + if (pz < minZ) { + len = (minX - px)*(minX - px) + (maxY - py)*(maxY - py) + (minZ - pz)*(minZ - pz); + return len < rScale; + } else if (pz < maxZ) { + len = (minX - px)*(minX - px) + (maxY - py)*(maxY - py); + return len < rScale; + } else if (pz > maxZ) { + len = (minX - px)*(minX - px) + (maxY - py)*(maxY - py) + (maxZ - pz)*(maxZ - pz); + return len < rScale; + } + } + } else if (px < maxX) { + if (py < minY) { + if (pz < minZ) { + len = (minY - py)*(minY - py) + (minZ - pz)*(minZ - pz); + return len < rScale; + } else if (pz < maxZ) { + len = (minY - py)*(minY - py); + return len < rScale; + } else if (pz > maxZ) { + len = (minY - py)*(minY - py) + (maxZ - pz)*(maxZ - pz); + return len < rScale; + } + } else if (py < maxY) { + if (pz < minZ) { + len = (minZ - pz)*(minZ - pz); + return len < rScale; + } else if (pz < maxZ) { + return true; + } else if (pz > maxZ) { + len = (maxZ - pz)*(maxZ - pz); + return len < rScale; + } + } else if (py > maxY) { + if (pz < minZ) { + len = (maxY - py)*(maxY - py) + (minZ - pz)*(minZ - pz); + return len < rScale; + } else if (pz < maxZ) { + len = (maxY - py)*(maxY - py); + return len < rScale; + } else if (pz > maxZ) { + len = (maxY - py)*(maxY - py) + (maxZ - pz)*(maxZ - pz); + return len < rScale; + } + } + } else if (px > maxX) { + if (py < minY) { + if (pz < minZ) { + len = (maxX - px)*(maxX - px) + (minY - py)*(minY - py) + (minZ - pz)*(minZ - pz); + return len < rScale; + } else if (pz < maxZ) { + len = (maxX - px)*(maxX - px) + (minY - py)*(minY - py); + return len < rScale; + } else if (pz > maxZ) { + len = (maxX - px)*(maxX - px) + (minY - py)*(minY - py) + (maxZ - pz)*(maxZ - pz); + return len < rScale; + } + } else if (py < maxY) { + if (pz < minZ) { + len = (maxX - px)*(maxX - px) + (minZ - pz)*(minZ - pz); + return len < rScale; + } else if (pz < maxZ) { + len = (maxX - px)*(maxX - px); + return len < rScale; + } else if (pz > maxZ) { + len = (maxX - px)*(maxX - px) + (maxZ - pz)*(maxZ - pz); + return len < rScale; + } + } else if (py > maxY) { + if (pz < minZ) { + len = (maxX - px)*(maxX - px) + (maxY - py)*(maxY - py) + (minZ - pz)*(minZ - pz); + return len < rScale; + } else if (pz < maxZ) { + len = (maxX - px)*(maxX - px) + (maxY - py)*(maxY - py); + return len < rScale; + } else if (pz > maxZ) { + len = (maxX - px)*(maxX - px) + (maxY - py)*(maxY - py) + (maxZ - pz)*(maxZ - pz); + return len < rScale; + } + } + } + return true; + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:OmniLight = new OmniLight(color, attenuationBegin, attenuationEnd); + res.clonePropertiesFrom(this); + return res; + } + + } +} diff --git a/src/alternativa/engine3d/lights/SpotLight.as b/src/alternativa/engine3d/lights/SpotLight.as new file mode 100644 index 0000000..303d093 --- /dev/null +++ b/src/alternativa/engine3d/lights/SpotLight.as @@ -0,0 +1,211 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.lights { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + + use namespace alternativa3d; + + /** + * OmniLight is an attenuated light source placed at one point and spreads outward in a coned direction. + * + * Lightning direction defines by z-axis of OmniLight. + * You can use lookAt() to make DirectionalLight point at given coordinates. + */ + public class SpotLight extends Light3D { + + /** + * Distance from which falloff starts. + */ + public var attenuationBegin:Number; + + /** + * Distance from at which falloff is complete. + */ + public var attenuationEnd:Number; + + /** + * Adjusts the angle of a light's cone. + */ + public var hotspot:Number; + + /** + * Adjusts the angle of a light's falloff. For photometric lights, the Field angle is comparable + * to the Falloff angle. It is the angle at which the light's intensity has fallen to zero. + */ + public var falloff:Number; + + /** + * Creates a new SpotLight instance. + * @param color Light color. + * @param attenuationBegin Distance from which falloff starts. + * @param attenuationEnd Distance from at which falloff is complete. + * @param hotspot Adjusts the angle of a light's cone. The Hotspot value is measured in radians. + * @param falloff Adjusts the angle of a light's falloff. The Falloff value is measured in radians. + */ + public function SpotLight(color:uint, attenuationBegin:Number, attenuationEnd:Number, hotspot:Number, falloff:Number) { + this.color = color; + this.attenuationBegin = attenuationBegin; + this.attenuationEnd = attenuationEnd; + this.hotspot = hotspot; + this.falloff = falloff; + calculateBoundBox(); + } + + /** + * @private + */ + override alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + var r:Number = (falloff < Math.PI) ? Math.sin(falloff*0.5)*attenuationEnd : attenuationEnd; + var bottom:Number = (falloff < Math.PI) ? 0 : Math.cos(falloff*0.5)*attenuationEnd; + boundBox.minX = -r; + boundBox.minY = -r; + boundBox.minZ = bottom; + boundBox.maxX = r; + boundBox.maxY = r; + boundBox.maxZ = attenuationEnd; + } + + /** + * Set direction of the light direction to the given coordinates.. + */ + public function lookAt(x:Number, y:Number, z:Number):void { + var dx:Number = x - this.x; + var dy:Number = y - this.y; + var dz:Number = z - this.z; + rotationX = Math.atan2(dz, Math.sqrt(dx*dx + dy*dy)) - Math.PI/2; + rotationY = 0; + rotationZ = -Math.atan2(dx, dy); + } + + /** + * @private + */ + override alternativa3d function checkBound(targetObject:Object3D):Boolean { + var minX:Number = boundBox.minX; + var minY:Number = boundBox.minY; + var minZ:Number = boundBox.minZ; + var maxX:Number = boundBox.maxX; + var maxY:Number = boundBox.maxY; + var maxZ:Number = boundBox.maxZ; + var sum:Number; + var pro:Number; + // Half sizes of the source's boundbox + var w:Number = (maxX - minX)*0.5; + var l:Number = (maxY - minY)*0.5; + var h:Number = (maxZ - minZ)*0.5; + // Half-vectors of the source's boundbox + var ax:Number = lightToObjectTransform.a*w; + var ay:Number = lightToObjectTransform.e*w; + var az:Number = lightToObjectTransform.i*w; + var bx:Number = lightToObjectTransform.b*l; + var by:Number = lightToObjectTransform.f*l; + var bz:Number = lightToObjectTransform.j*l; + var cx:Number = lightToObjectTransform.c*h; + var cy:Number = lightToObjectTransform.g*h; + var cz:Number = lightToObjectTransform.k*h; + // Half sizes of the boundboxes + var objectBB:BoundBox = targetObject.boundBox; + var hw:Number = (objectBB.maxX - objectBB.minX)*0.5; + var hl:Number = (objectBB.maxY - objectBB.minY)*0.5; + var hh:Number = (objectBB.maxZ - objectBB.minZ)*0.5; + // Vector between centers of the bounboxes + var dx:Number = lightToObjectTransform.a*(minX + w) + lightToObjectTransform.b*(minY + l) + lightToObjectTransform.c*(minZ + h) + lightToObjectTransform.d - objectBB.minX - hw; + var dy:Number = lightToObjectTransform.e*(minX + w) + lightToObjectTransform.f*(minY + l) + lightToObjectTransform.g*(minZ + h) + lightToObjectTransform.h - objectBB.minY - hl; + var dz:Number = lightToObjectTransform.i*(minX + w) + lightToObjectTransform.j*(minY + l) + lightToObjectTransform.k*(minZ + h) + lightToObjectTransform.l - objectBB.minZ - hh; + + // X of the object + sum = 0; + if (ax >= 0) sum += ax; else sum -= ax; + if (bx >= 0) sum += bx; else sum -= bx; + if (cx >= 0) sum += cx; else sum -= cx; + sum += hw; + if (dx >= 0) sum -= dx; + sum += dx; + if (sum <= 0) return false; + // Y of the object + sum = 0; + if (ay >= 0) sum += ay; else sum -= ay; + if (by >= 0) sum += by; else sum -= by; + if (cy >= 0) sum += cy; else sum -= cy; + sum += hl; + if (dy >= 0) sum -= dy; else sum += dy; + if (sum <= 0) return false; + // Z of the object + sum = 0; + if (az >= 0) sum += az; else sum -= az; + if (bz >= 0) sum += bz; else sum -= bz; + if (cz >= 0) sum += cz; else sum -= cz; + sum += hl; + if (dz >= 0) sum -= dz; else sum += dz; + if (sum <= 0) return false; + // X of the source + sum = 0; + pro = lightToObjectTransform.a*ax + lightToObjectTransform.e*ay + lightToObjectTransform.i*az; + if (pro >= 0) sum += pro; else sum -= pro; + pro = lightToObjectTransform.a*bx + lightToObjectTransform.e*by + lightToObjectTransform.i*bz; + if (pro >= 0) sum += pro; else sum -= pro; + pro = lightToObjectTransform.a*cx + lightToObjectTransform.e*cy + lightToObjectTransform.i*cz; + if (pro >= 0) sum += pro; else sum -= pro; + if (lightToObjectTransform.a >= 0) sum += lightToObjectTransform.a*hw; else sum -= lightToObjectTransform.a*hw; + if (lightToObjectTransform.e >= 0) sum += lightToObjectTransform.e*hl; else sum -= lightToObjectTransform.e*hl; + if (lightToObjectTransform.i >= 0) sum += lightToObjectTransform.i*hh; else sum -= lightToObjectTransform.i*hh; + pro = lightToObjectTransform.a*dx + lightToObjectTransform.e*dy + lightToObjectTransform.i*dz; + if (pro >= 0) sum -= pro; else sum += pro; + if (sum <= 0) return false; + // Y of the source + sum = 0; + pro = lightToObjectTransform.b*ax + lightToObjectTransform.f*ay + lightToObjectTransform.j*az; + if (pro >= 0) sum += pro; else sum -= pro; + pro = lightToObjectTransform.b*bx + lightToObjectTransform.f*by + lightToObjectTransform.j*bz; + if (pro >= 0) sum += pro; else sum -= pro; + pro = lightToObjectTransform.b*cx + lightToObjectTransform.f*cy + lightToObjectTransform.j*cz; + if (pro >= 0) sum += pro; else sum -= pro; + if (lightToObjectTransform.b >= 0) sum += lightToObjectTransform.b*hw; else sum -= lightToObjectTransform.b*hw; + if (lightToObjectTransform.f >= 0) sum += lightToObjectTransform.f*hl; else sum -= lightToObjectTransform.f*hl; + if (lightToObjectTransform.j >= 0) sum += lightToObjectTransform.j*hh; else sum -= lightToObjectTransform.j*hh; + pro = lightToObjectTransform.b*dx + lightToObjectTransform.f*dy + lightToObjectTransform.j*dz; + if (pro >= 0) sum -= pro; + sum += pro; + if (sum <= 0) return false; + // Z of the source + sum = 0; + pro = lightToObjectTransform.c*ax + lightToObjectTransform.g*ay + lightToObjectTransform.k*az; + if (pro >= 0) sum += pro; else sum -= pro; + pro = lightToObjectTransform.c*bx + lightToObjectTransform.g*by + lightToObjectTransform.k*bz; + if (pro >= 0) sum += pro; else sum -= pro; + pro = lightToObjectTransform.c*cx + lightToObjectTransform.g*cy + lightToObjectTransform.k*cz; + if (pro >= 0) sum += pro; else sum -= pro; + if (lightToObjectTransform.c >= 0) sum += lightToObjectTransform.c*hw; else sum -= lightToObjectTransform.c*hw; + if (lightToObjectTransform.g >= 0) sum += lightToObjectTransform.g*hl; else sum -= lightToObjectTransform.g*hl; + if (lightToObjectTransform.k >= 0) sum += lightToObjectTransform.k*hh; else sum -= lightToObjectTransform.k*hh; + pro = lightToObjectTransform.c*dx + lightToObjectTransform.g*dy + lightToObjectTransform.k*dz; + if (pro >= 0) sum -= pro; else sum += pro; + if (sum <= 0) return false; + // TODO: checking on random axises + return true; + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:SpotLight = new SpotLight(color, attenuationBegin, attenuationEnd, hotspot, falloff); + res.clonePropertiesFrom(this); + return res; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/ExporterA3D.as b/src/alternativa/engine3d/loaders/ExporterA3D.as new file mode 100644 index 0000000..35a3dfb --- /dev/null +++ b/src/alternativa/engine3d/loaders/ExporterA3D.as @@ -0,0 +1,633 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.animation.AnimationClip; + import alternativa.engine3d.animation.keys.TransformKey; + import alternativa.engine3d.animation.keys.TransformTrack; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.core.VertexStream; + import alternativa.engine3d.lights.AmbientLight; + import alternativa.engine3d.lights.DirectionalLight; + import alternativa.engine3d.lights.OmniLight; + import alternativa.engine3d.lights.SpotLight; + import alternativa.engine3d.materials.LightMapMaterial; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.materials.StandardMaterial; + import alternativa.engine3d.materials.TextureMaterial; + import alternativa.engine3d.objects.Joint; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.objects.Skin; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.ExternalTextureResource; + import alternativa.engine3d.resources.Geometry; + import alternativa.engine3d.resources.TextureResource; + import alternativa.osgi.OSGi; + import alternativa.osgi.service.clientlog.IClientLog; + import alternativa.protocol.CompressionType; + import alternativa.protocol.ICodec; + import alternativa.protocol.IProtocol; + import alternativa.protocol.OptionalMap; + import alternativa.protocol.ProtocolBuffer; + import alternativa.protocol.impl.PacketHelper; + import alternativa.protocol.impl.Protocol; + import alternativa.protocol.info.TypeCodecInfo; + import alternativa.protocol.osgi.ProtocolActivator; + import alternativa.types.Long; + + import commons.A3DMatrix; + + import flash.geom.Matrix3D; + import flash.utils.ByteArray; + import flash.utils.Dictionary; + import flash.utils.Endian; + + import platform.client.formats.a3d.osgi.Activator; + import platform.clients.fp10.libraries.alternativaprotocol.Activator; + + import versions.version2.a3d.A3D2; + import versions.version2.a3d.animation.A3D2AnimationClip; + import versions.version2.a3d.animation.A3D2Keyframe; + import versions.version2.a3d.animation.A3D2Track; + import versions.version2.a3d.geometry.A3D2IndexBuffer; + import versions.version2.a3d.geometry.A3D2VertexAttributes; + import versions.version2.a3d.geometry.A3D2VertexBuffer; + import versions.version2.a3d.materials.A3D2Image; + import versions.version2.a3d.materials.A3D2Map; + import versions.version2.a3d.materials.A3D2Material; + import versions.version2.a3d.objects.A3D2AmbientLight; + import versions.version2.a3d.objects.A3D2Box; + import versions.version2.a3d.objects.A3D2DirectionalLight; + import versions.version2.a3d.objects.A3D2Joint; + import versions.version2.a3d.objects.A3D2JointBindTransform; + import versions.version2.a3d.objects.A3D2Mesh; + import versions.version2.a3d.objects.A3D2Object; + import versions.version2.a3d.objects.A3D2OmniLight; + import versions.version2.a3d.objects.A3D2Skin; + import versions.version2.a3d.objects.A3D2SpotLight; + import versions.version2.a3d.objects.A3D2Surface; + import versions.version2.a3d.objects.A3D2Transform; + + use namespace alternativa3d; + + /** + * An object which allows to convert hierarchy of three-dimensional objects to binary A3D format. + */ + public class ExporterA3D { + + private var wasInit:Boolean = false; + private var protocol:Protocol; + private var parents:Dictionary; + private var geometries:Dictionary; + private var images:Dictionary; + + private var indexBufferID:int; + private var materialID:int; + private var mapID:int; + private var imageID:int; + private var animationID:int; + private var trackID:int; + private var vertexBufferID:int; + private var boxID:int; + + private var tracksMap:Dictionary; + private var materialsMap:Dictionary; + private var mapsMap:Dictionary; + + alternativa3d var idGenerator:IIDGenerator = new IncrementalIDGenerator(); + + /** + * Creates an instance of ExporterA3D. + */ + public function ExporterA3D() { + init(); + } + + private function init():void { + if (wasInit) return; + if (OSGi.getInstance() != null) { + protocol = Protocol(OSGi.getInstance().getService(IProtocol)); + return; + } + OSGi.clientLog = new DummyClientLog(); + var osgi:OSGi = new OSGi(); + osgi.registerService(IClientLog, new DummyClientLog()); + + new ProtocolActivator().start(osgi); + new platform.client.formats.a3d.osgi.Activator().start(osgi); + new platform.clients.fp10.libraries.alternativaprotocol.Activator().start(osgi); + protocol = Protocol(osgi.getService(IProtocol)); + wasInit = true; + } + + /** + * Exports a scene to A3D format. + * @param root Root object of scene. + * @return Data in A3D format. + */ + public function export(root:Object3D = null, animations:Vector. = null):ByteArray { + + boxID = 0; + indexBufferID = 0; + vertexBufferID = 0; + materialID = 0; + mapID = 0; + imageID = 0; + animationID = 0; + materialsMap = new Dictionary(); + mapsMap = new Dictionary(); + geometries = new Dictionary(); + tracksMap = new Dictionary(); + + images = new Dictionary(); + + parents = new Dictionary(); + + var a3D:A3D2 = new A3D2( + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + if (root != null) { + exportHierarchy(root, a3D); + } + if (animations != null) { + for each (var animation:AnimationClip in animations) { + exportAnimation(animation, a3D); + } + } + + var data:ByteArray = new ByteArray(); + var result:ByteArray = new ByteArray(); + var codec:ICodec = protocol.getCodec(new TypeCodecInfo(A3D2, false)); + + var protocolBuffer:ProtocolBuffer = new ProtocolBuffer(data, data, new OptionalMap()); + data.writeShort(2); + data.writeShort(0); + codec.encode(protocolBuffer, a3D); + data.position = 0; + PacketHelper.wrapPacket(result, protocolBuffer, CompressionType.DEFLATE); + return result; + } + + private function exportAnimation(source:AnimationClip, dest:A3D2):void { + var anim:A3D2AnimationClip = new A3D2AnimationClip(animationID, source.loop, source.name, null, exportTracks(source, dest)); + if (dest.animationClips == null) dest.animationClips = new Vector.(); + dest.animationClips[animationID] = anim; animationID++; + } + + private function exportTracks(source:AnimationClip, dest:A3D2):Vector. { + var id:int; + var result:Vector. = new Vector.(); + for (var i:int = 0; i < source.numTracks; i++) { + var t:TransformTrack = source.getTrackAt(i) as TransformTrack; + if (t != null && tracksMap[t] == null) { + id = trackID++; + var exportTrack:A3D2Track = new A3D2Track(id, exportKeyframes(t), t.object); + tracksMap[t] = id; + if (dest.animationTracks == null) dest.animationTracks = new Vector.(); + dest.animationTracks[id] = exportTrack; + } else { + id = tracksMap[t]; + } + result.push(id); + } + return result; + } + + private function exportKeyframes(source:TransformTrack):Vector. { + var result:Vector. = new Vector.(); + for (var key:TransformKey = TransformKey(source.keyFramesList); key.next != null; key = key.next) { + var exportKey:A3D2Keyframe = new A3D2Keyframe(key._time, exportTransformFromKeyframe(key)); + result.push(exportKey); + } + return result; + } + + private function exportTransformFromKeyframe(key:TransformKey):A3D2Transform { + var m:Matrix3D = Matrix3D(key.value); + var vec:Vector. = m.rawData; + var exportTransform:A3D2Transform = new A3D2Transform( + new A3DMatrix( + vec[0], vec[4], vec[8], vec[12], + vec[1], vec[5], vec[9], vec[13], + vec[2], vec[6], vec[10], vec[14] + )); + return exportTransform; + } + + + + private function exportHierarchy(source:Object3D, dest:A3D2):void { + var id:Long = idGenerator.getID(source); + if (source.transformChanged) { + source.composeTransforms(); + } + if (source is SpotLight) { + exportSpotLight(id, source as SpotLight, dest); + } else if (source is OmniLight) { + exportOmniLight(id, source as OmniLight, dest); + } else if (source is DirectionalLight) { + exportDirLight(id, source as DirectionalLight, dest); + } else if (source is AmbientLight) { + exportAmbientLight(id, source as AmbientLight, dest); + } else if (source is Skin) { + exportSkin(id, source as Skin, dest); + } else if (source is Mesh) { + exportMesh(id, source as Mesh, dest); + } else if (source is Joint) { + exportJoint(id, source as Joint, dest); + } else if (source is Object3D) { + exportObject3D(id, source, dest); + } else { + trace("Unsupported object type", source); + } + parents[source] = id; + + for (var child:Object3D = source.childrenList; child != null; child = child.next) { + exportHierarchy(child, dest); + } + + } + + private function exportJoint(id:Long, source:Joint, dest:A3D2):void { + + var a3DObject:A3D2Joint = new A3D2Joint( + exportBoundBox(source.boundBox, dest), + id, + source.name, + parents[source.parent is Skin ? source.parent.parent : source.parent], + exportTransform(source.transform), + source.visible + ); + if (dest.joints == null) dest.joints = new Vector.(); + dest.joints.push(a3DObject); + } + + private function exportObject3D(id:Long, source:Object3D, dest:A3D2):void { + var a3DObject:A3D2Object = new A3D2Object( + exportBoundBox(source.boundBox, dest), + id, + source.name, + parents[source.parent], + exportTransform(source.transform), + source.visible + ); + if (dest.objects == null) dest.objects = new Vector.(); + dest.objects.push(a3DObject); + } + + private function exportSpotLight(id:Long, source:SpotLight, dest:A3D2):void { + var a3DObject:A3D2SpotLight = new A3D2SpotLight( + source.attenuationBegin, + source.attenuationEnd, + exportBoundBox(source.boundBox, dest), + source.color, + source.falloff, + source.hotspot, + id, + source.intensity, + source.name, + parents[source.parent], + exportTransform(source.transform), + source.visible + ); + if (dest.spotLights == null) dest.spotLights = new Vector.(); + dest.spotLights.push(a3DObject); + } + + private function exportOmniLight(id:Long, source:OmniLight, dest:A3D2):void { + var a3DObject:A3D2OmniLight = new A3D2OmniLight( + source.attenuationBegin, + source.attenuationEnd, + exportBoundBox(source.boundBox, dest), + source.color, + id, + source.intensity, + source.name, + parents[source.parent], + exportTransform(source.transform), + source.visible + ); + if (dest.omniLights == null) dest.omniLights = new Vector.(); + dest.omniLights.push(a3DObject); + } + + private function exportDirLight(id:Long, source:DirectionalLight, dest:A3D2):void { + var a3DObject:A3D2DirectionalLight = new A3D2DirectionalLight( + exportBoundBox(source.boundBox, dest), + source.color, + id, + source.intensity, + source.name, + parents[source.parent], + exportTransform(source.transform), + source.visible + ); + if (dest.directionalLights == null) dest.directionalLights = new Vector.(); + dest.directionalLights.push(a3DObject); + + } + + private function exportAmbientLight(id:Long, source:AmbientLight, dest:A3D2):void { + var a3DObject:A3D2AmbientLight = new A3D2AmbientLight( + exportBoundBox(source.boundBox, dest), + source.color, + id, + source.intensity, + source.name, + parents[source.parent], + exportTransform(source.transform), + source.visible + ); + if (dest.ambientLights == null) dest.ambientLights = new Vector.(); + dest.ambientLights.push(a3DObject); + } + + private function exportMesh(id:Long, source:Mesh, dest:A3D2):void { + var geometryData:GeometryData = exportGeometry(source.geometry, dest); + var a3DMesh:A3D2Mesh = new A3D2Mesh( + exportBoundBox(source.boundBox, dest), + id, + geometryData.indexBufferID, + source.name, + parents[source.parent], + exportSurfaces(source._surfaces, dest), + exportTransform(source.transform), + geometryData.vertexBufferIDs, + source.visible + ); + if (dest.meshes == null) dest.meshes = new Vector.(); + dest.meshes.push(a3DMesh); + } + + private function exportSkin(id:Long, source:Skin, dest:A3D2):A3D2Skin { + var geometryData:GeometryData = exportGeometry(source.geometry, dest); + var a3DSkin:A3D2Skin = new A3D2Skin( + exportBoundBox(source.boundBox, dest), + id, + geometryData.indexBufferID, + exportJointsBindTransforms(source._renderedJoints), + exportJointsListFromSurfacesJoints(source.surfaceJoints), + source.name, + exportNumJoitns(source.surfaceJoints), + null, + exportSurfaces(source._surfaces, dest), + exportTransform(source.transform), + geometryData.vertexBufferIDs, + source.visible + ); + if (dest.skins == null) dest.skins = new Vector.(); + dest.skins.push(a3DSkin); + return a3DSkin; + } + + private function exportNumJoitns(surfaceJoints:Vector.>):Vector. { + var result:Vector. = new Vector.(); + for (var i:int = 0; i < surfaceJoints.length; i++) { + result.push(surfaceJoints[i].length); + } + return result; + } + + private function exportJointsBindTransforms(joints:Vector.):Vector. { + var result:Vector. = new Vector.(); + for each (var joint:Joint in joints) { + result.push(new A3D2JointBindTransform(exportTransform(joint.bindPoseTransform), idGenerator.getID(joint))); + } + return result; + } + + private function exportJointsListFromSurfacesJoints(surfaceJoints:Vector.>):Vector. { + var result:Vector. = new Vector.(); + for (var i:int = 0; i < surfaceJoints.length; i++) { + var joints:Vector. = surfaceJoints[i]; + for each (var joint:Joint in joints) { + result.push(idGenerator.getID(joint)); + } + } + return result; + } + + private function exportSurfaces(surfaces:Vector., dest:A3D2):Vector. { + var result:Vector. = new Vector.(); + for (var i:int = 0, count:int = surfaces.length; i < count; i++) { + var surface:Surface = surfaces[i]; + var resSurface:A3D2Surface = new A3D2Surface(surface.indexBegin, exportMaterial(surface.material, dest), surface.numTriangles); + result[i] = resSurface; + } + return result; + } + + private function exportMaterial(source:Material, dest:A3D2):int { + if (source == null) return -1; + var result:A3D2Material = materialsMap[source]; + if (result != null) return result.id; + if (source is ParserMaterial) { + var parserMaterial:ParserMaterial = source as ParserMaterial; + result = new A3D2Material( + exportMap(parserMaterial.textures["diffuse"], 0, dest), + exportMap(parserMaterial.textures["glossiness"], 0, dest), + materialID, + exportMap(parserMaterial.textures["emission"], 0, dest), + exportMap(parserMaterial.textures["bump"], 0, dest), + exportMap(parserMaterial.textures["transparent"], 0, dest), + -1, + exportMap(parserMaterial.textures["specular"], 0, dest) + ); + } else if (source is LightMapMaterial) { + var lightMapMaterial:LightMapMaterial = source as LightMapMaterial; + result = new A3D2Material( + exportMap(lightMapMaterial.diffuseMap, 0, dest), + -1, + materialID, + exportMap(lightMapMaterial.lightMap, lightMapMaterial.lightMapChannel, dest), + -1, + exportMap(lightMapMaterial.opacityMap, 0, dest), + -1, + -1); + } else if (source is StandardMaterial) { + var standardMaterial:StandardMaterial = source as StandardMaterial; + result = new A3D2Material( + exportMap(standardMaterial.diffuseMap, 0, dest), + exportMap(standardMaterial.glossinessMap, 0, dest), materialID, + -1, + exportMap(standardMaterial.normalMap, 0, dest), + exportMap(standardMaterial.opacityMap, 0, dest), + -1, + exportMap(standardMaterial.specularMap, 0, dest)); + } else if (source is TextureMaterial) { + var textureMaterial:TextureMaterial = source as TextureMaterial; + result = new A3D2Material(exportMap(textureMaterial.diffuseMap, 0, dest), -1, materialID, -1, -1, exportMap(textureMaterial.opacityMap, 0, dest), -1, -1); + } + materialsMap[source] = result; + if (dest.materials == null) dest.materials = new Vector.(); + dest.materials[materialID] = result; + return materialID++; + } + + private function exportMap(source:TextureResource, channel:int, dest:A3D2):int { + if (source == null) return -1; + var result:A3D2Map = mapsMap[source]; + if (result != null) return result.id; + if (source is ExternalTextureResource) { + var resource:ExternalTextureResource = source as ExternalTextureResource; + result = new A3D2Map(channel, mapID, exportImage(resource, dest)); + if (dest.maps == null) dest.maps = new Vector.(); + dest.maps[mapID] = result; + mapsMap[source] = result; + + return mapID++; + } + return -1; + } + + private function exportImage(source:ExternalTextureResource, dest:A3D2):int { + var image:Object = images[source]; + if (image != null) return int(image); + var result:A3D2Image = new A3D2Image(imageID, source.url); + if (dest.images == null) dest.images = new Vector.(); + dest.images[imageID] = result; + return imageID++; + } + + private function exportGeometry(geometry:Geometry, dest:A3D2):GeometryData { + var result:GeometryData = geometries[geometry]; + if (result != null) return result; + result = new GeometryData(); + result.vertexBufferIDs = new Vector.(); + var indicesData:ByteArray = new ByteArray(); + indicesData.endian = Endian.LITTLE_ENDIAN; + var indices:Vector. = geometry.indices; + for (var i:int = 0, count:int = indices.length; i < count; i++) { + indicesData.writeShort(indices[i]); + } + var indexBuffer:A3D2IndexBuffer = new A3D2IndexBuffer(indicesData, indexBufferID, indices.length); + result.indexBufferID = indexBufferID; + if (dest.indexBuffers == null) dest.indexBuffers = new Vector.(); + dest.indexBuffers[indexBufferID] = indexBuffer; + indexBufferID++; + for (i = 0,count = geometry._vertexStreams.length; i < count; i++) { + var stream:VertexStream = geometry._vertexStreams[i]; + var buffer:A3D2VertexBuffer = new A3D2VertexBuffer(exportAttributes(stream.attributes), stream.data, vertexBufferID, geometry.numVertices); + if (dest.vertexBuffers == null) dest.vertexBuffers = new Vector.(); + dest.vertexBuffers[vertexBufferID] = buffer; + result.vertexBufferIDs[i] = vertexBufferID++; + } + return result; + } + + private function exportAttributes(attributes:Array):Vector. { + var prev:int = -1; + var result:Vector. = new Vector.(); + for each (var attr:int in attributes) { + if (attr == prev) continue; + switch (attr) { + case VertexAttributes.POSITION: + result.push(A3D2VertexAttributes.POSITION); + break; + case VertexAttributes.NORMAL: + result.push(A3D2VertexAttributes.NORMAL); + break; + case VertexAttributes.TANGENT4: + result.push(A3D2VertexAttributes.TANGENT4); + break; + default: + if ((attr >= VertexAttributes.JOINTS[0]) && (attr <= VertexAttributes.JOINTS[3])) { + result.push(A3D2VertexAttributes.JOINT); + } else if ((attr >= VertexAttributes.TEXCOORDS[0]) && (attr <= VertexAttributes.TEXCOORDS[7])) { + result.push(A3D2VertexAttributes.TEXCOORD); + } + break; + } + prev = attr; + } + return result; + } + + private function exportTransform(source:Transform3D):A3D2Transform { + return new A3D2Transform(new A3DMatrix( + source.a, source.b, source.c, source.d, + source.e, source.f, source.g, source.h, + source.i, source.j, source.k, source.l + )); + } + + private function exportBoundBox(boundBox:BoundBox, dest:A3D2):int { + if (boundBox == null) return -1; + if (dest.boxes == null) dest.boxes = new Vector.(); + dest.boxes[boxID] = new A3D2Box(Vector.([boundBox.minX, boundBox.minY, boundBox.minZ, boundBox.maxX, boundBox.maxY, boundBox.maxZ]), boxID); + return boxID++; + } + } +} + +import alternativa.osgi.service.clientlog.IClientLog; +import alternativa.osgi.service.clientlog.IClientLogChannelListener; + +class GeometryData { + public var indexBufferID:int; + public var vertexBufferIDs:Vector.; + + public function GeometryData(indexBufferID:int = -1, vertexBufferIDs:Vector. = null) { + this.indexBufferID = indexBufferID; + this.vertexBufferIDs = vertexBufferIDs; + } +} + +class DummyClientLog implements IClientLog { + + public function logError(channelName:String, text:String, ... vars):void { + } + + public function log(channelName:String, text:String, ... rest):void { + } + + public function getChannelStrings(channelName:String):Vector. { + return null; + } + + public function addLogListener(listener:IClientLogChannelListener):void { + } + + public function removeLogListener(listener:IClientLogChannelListener):void { + } + + public function addLogChannelListener(channelName:String, listener:IClientLogChannelListener):void { + } + + public function removeLogChannelListener(channelName:String, listener:IClientLogChannelListener):void { + } + + public function getChannelNames():Vector. { + return null; + } +} diff --git a/src/alternativa/engine3d/loaders/IIDGenerator.as b/src/alternativa/engine3d/loaders/IIDGenerator.as new file mode 100644 index 0000000..7022fee --- /dev/null +++ b/src/alternativa/engine3d/loaders/IIDGenerator.as @@ -0,0 +1,23 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders { + + import alternativa.engine3d.core.Object3D; + import alternativa.types.Long; + + /** + * @private + */ + public interface IIDGenerator { + function getID(object:Object3D):Long; + + } +} diff --git a/src/alternativa/engine3d/loaders/IncrementalIDGenerator.as b/src/alternativa/engine3d/loaders/IncrementalIDGenerator.as new file mode 100644 index 0000000..48edae9 --- /dev/null +++ b/src/alternativa/engine3d/loaders/IncrementalIDGenerator.as @@ -0,0 +1,38 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders { + + import alternativa.engine3d.core.Object3D; + import alternativa.types.Long; + + import flash.utils.Dictionary; + + /** + * @private + */ + public class IncrementalIDGenerator implements IIDGenerator { + + private var lastID:uint = 0; + private var objects:Dictionary; + + public function IncrementalIDGenerator() { + objects = new Dictionary(true); + } + + public function getID(object:Object3D):Long { + var result:Long = objects[object]; + if (result == null) { + result = objects[object] = Long.fromInt(lastID); lastID++; + } + return result; + } + } +} diff --git a/src/alternativa/engine3d/loaders/Parser.as b/src/alternativa/engine3d/loaders/Parser.as new file mode 100644 index 0000000..197f2ac --- /dev/null +++ b/src/alternativa/engine3d/loaders/Parser.as @@ -0,0 +1,1067 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.animation.AnimationClip; + import alternativa.engine3d.animation.keys.Track; + import alternativa.engine3d.animation.keys.TransformKey; + import alternativa.engine3d.animation.keys.TransformTrack; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.core.VertexStream; + import alternativa.engine3d.lights.AmbientLight; + import alternativa.engine3d.lights.DirectionalLight; + import alternativa.engine3d.lights.OmniLight; + import alternativa.engine3d.lights.SpotLight; + import alternativa.engine3d.materials.A3DUtils; + import alternativa.engine3d.objects.Joint; + import alternativa.engine3d.objects.LOD; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.objects.Skin; + import alternativa.engine3d.objects.Sprite3D; + import alternativa.engine3d.resources.ExternalTextureResource; + import alternativa.engine3d.resources.Geometry; + import alternativa.types.Long; + + import commons.A3DMatrix; + import commons.Id; + + import flash.geom.Matrix3D; + import flash.geom.Orientation3D; + import flash.geom.Vector3D; + import flash.utils.ByteArray; + import flash.utils.Dictionary; + import flash.utils.Endian; + + import versions.version1.a3d.A3D; + import versions.version1.a3d.geometry.A3DGeometry; + import versions.version1.a3d.geometry.A3DIndexBuffer; + import versions.version1.a3d.geometry.A3DVertexBuffer; + import versions.version1.a3d.id.ParentId; + import versions.version1.a3d.materials.A3DImage; + import versions.version1.a3d.materials.A3DMap; + import versions.version1.a3d.materials.A3DMaterial; + import versions.version1.a3d.objects.A3DBox; + import versions.version1.a3d.objects.A3DObject; + import versions.version1.a3d.objects.A3DSurface; + import versions.version2.a3d.A3D2; + import versions.version2.a3d.A3D2Extra1; + import versions.version2.a3d.A3D2Extra2; + import versions.version2.a3d.animation.A3D2AnimationClip; + import versions.version2.a3d.animation.A3D2Keyframe; + import versions.version2.a3d.animation.A3D2Track; + import versions.version2.a3d.geometry.A3D2IndexBuffer; + import versions.version2.a3d.geometry.A3D2VertexAttributes; + import versions.version2.a3d.geometry.A3D2VertexBuffer; + import versions.version2.a3d.materials.A3D2CubeMap; + import versions.version2.a3d.materials.A3D2Image; + import versions.version2.a3d.materials.A3D2Map; + import versions.version2.a3d.materials.A3D2Material; + import versions.version2.a3d.objects.A3D2AmbientLight; + import versions.version2.a3d.objects.A3D2Box; + import versions.version2.a3d.objects.A3D2DirectionalLight; + import versions.version2.a3d.objects.A3D2Joint; + import versions.version2.a3d.objects.A3D2JointBindTransform; + import versions.version2.a3d.objects.A3D2LOD; + import versions.version2.a3d.objects.A3D2Layer; + import versions.version2.a3d.objects.A3D2Mesh; + import versions.version2.a3d.objects.A3D2Object; + import versions.version2.a3d.objects.A3D2OmniLight; + import versions.version2.a3d.objects.A3D2Skin; + import versions.version2.a3d.objects.A3D2SpotLight; + import versions.version2.a3d.objects.A3D2Sprite; + import versions.version2.a3d.objects.A3D2Surface; + import versions.version2.a3d.objects.A3D2Transform; + + use namespace alternativa3d; + + /** + * Base class for classes, that perform parsing of scenes of different formats. + */ + public class Parser { + + /** + * List of root objects. Root objects are objects, that have no parents. + * @see alternativa.engine3d.core.Object3D + */ + public var hierarchy:Vector.; + /** + * List of objects, that are got after parsing. + * @see alternativa.engine3d.core.Object3D + */ + public var objects:Vector.; + + /** + * Array of animations. + */ + public var animations:Vector.; + + /** + * List of all materials assigned to objects, that are got after parsing. + * @see alternativa.engine3d.loaders.ParserMaterial + */ + public var materials:Vector.; + + private var maps:Dictionary; + + private var cubemaps:Dictionary; + + /** + * @private + */ + alternativa3d var layersMap:Dictionary; + + alternativa3d var layers:Vector.; + + alternativa3d var compressedBuffers:Boolean = false; + + private var parsedMaterials:Dictionary; + private var parsedGeometries:Object; + private var unpackedBuffers:Dictionary; + + /** + * Returns object from array objects by name. + */ + public function getObjectByName(name:String):Object3D { + for each (var object:Object3D in objects) { + if (object.name == name) return object; + } + return null; + } + + /** + * Returns name of layer for specified object. + */ + public function getLayerByObject(object:Object3D):String { + return layersMap[object]; + } + + /** + * Erases all links to external objects. + */ + public function clean():void { + hierarchy = null; + objects = null; + materials = null; + animations = null; + layersMap = null; + objectsMap = null; + a3DBoxes = null; + parents = null; + layers = null; + } + + /** + * @private + */ + alternativa3d function init():void { + hierarchy = new Vector.(); + objects = new Vector.(); + materials = new Vector.(); + animations = new Vector.(); + layersMap = new Dictionary(true); + layers = new Vector.(); + } + + protected function complete(a3d:Object):void { + init(); + if (a3d is A3D) { + doParse2_0(convert1_2(A3D(a3d))); + } else if (a3d is A3D2) { + doParse2_0(A3D2(a3d)); + } else if (a3d is Vector.) { + var vec:Vector. = a3d as Vector.; + var len:int = vec.length; + for (var i:int = 0; i < len; i++) { + doParsePart(vec[i]); + } + } + completeHierarchy(); + } + + private function doParsePart(a3d:Object):void { + if (a3d is A3D) { + doParse2_0(convert1_2(A3D(a3d))); + } else if (a3d is A3D2) { + doParse2_0(A3D2(a3d)); + } else if (a3d is A3D2Extra1) { + doParseExtra1(A3D2Extra1(a3d)); + } else if (a3d is A3D2Extra2) { + doParseExtra2(A3D2Extra2(a3d)); + } + } + + private function doParseExtra1(a3d:A3D2Extra1):void { + var layersVec:Vector. = a3d.layers; + for each (var layer:A3D2Layer in layersVec) { + var layerName:String = (layer.name == null || layer.name.length == 0) ? "default" : layer.name; + layers.push(layerName); + for each (var id:Long in layer.objects) { + if (objectsMap[id] != null) { + layersMap[objectsMap[id]] = layerName; + } + } + } + } + + private function doParseExtra2(a3d:A3D2Extra2):void { + var lodsVec:Vector. = a3d.lods; + for each (var lod:A3D2LOD in lodsVec) { + var resObject:LOD = new LOD(); + resObject.visible = lod.visible; + resObject.name = lod.name; + parents[resObject] = lod.parentId; + objectsMap[lod.id] = resObject; + var length:uint = lod.objects.length; + for (var i:int = 0; i < length; i++) { + resObject.addLevel(objectsMap[lod.objects[i]], lod.distances[i]); + } + decomposeTransformation(lod.transform, resObject); + + } + } + + private var objectsMap:Dictionary; + + private var parents:Dictionary = new Dictionary(); + + private var a3DBoxes:Dictionary = new Dictionary(); + + private function doParse2_0(a3d:A3D2):void { + maps = new Dictionary(); + cubemaps = new Dictionary(); + parsedMaterials = new Dictionary(); + parsedGeometries = new Dictionary(); + unpackedBuffers = new Dictionary(); + objectsMap = new Dictionary(); + parents = new Dictionary(); + a3DBoxes = new Dictionary(); + + + var parsedTracks:Dictionary = new Dictionary(); + var a3DIndexBuffers:Dictionary = new Dictionary(); + var a3DVertexBuffers:Dictionary = new Dictionary(); + var a3DMaterials:Dictionary = new Dictionary(); + var a3DMaps:Dictionary = new Dictionary(); + var a3DImages:Dictionary = new Dictionary(); + var a3DCubeMaps:Dictionary = new Dictionary(); + var a3DObject:A3D2Object; + var a3DMesh:A3D2Mesh; + + var a3DIndexBuffer:A3D2IndexBuffer; + var a3DVertexBuffer:A3D2VertexBuffer; + var a3DMaterial:A3D2Material; + var a3DBox:A3D2Box; + var a3DMap:A3D2Map; + var a3DImage:A3D2Image; + var a3DAmbientLight:A3D2AmbientLight; + var a3DOmniLight:A3D2OmniLight; + var a3DSpotLight:A3D2SpotLight; + var a3DDirLight:A3D2DirectionalLight; + var a3DSkin:A3D2Skin; + var a3DJoint:A3D2Joint; + var a3DSprite:A3D2Sprite; + var a3DCubeMap:A3D2CubeMap; + + for each(a3DIndexBuffer in a3d.indexBuffers) { + a3DIndexBuffers[a3DIndexBuffer.id] = a3DIndexBuffer; + } + for each (var a3DTrack:A3D2Track in a3d.animationTracks) { + var resTrack:TransformTrack = new TransformTrack(a3DTrack.objectName); + for each (var a3DKeyFrame:A3D2Keyframe in a3DTrack.keyframes) { + var tFrame:TransformKey = new TransformKey(); + tFrame._time = a3DKeyFrame.time; + + var components:Vector. = getMatrix3D(a3DKeyFrame.transform).decompose(Orientation3D.QUATERNION); + + tFrame.x = components[0].x; + tFrame.y = components[0].y; + tFrame.z = components[0].z; + tFrame.rotation = components[1]; + tFrame.scaleX = components[2].x; + tFrame.scaleY = components[2].y; + tFrame.scaleZ = components[2].z; + resTrack.addKeyToList(tFrame); + } + parsedTracks[a3DTrack.id] = resTrack; + } + + var animationClip:AnimationClip; + + // Animation parsing + if (a3d.animationTracks != null && a3d.animationTracks.length > 0) { + if (a3d.animationClips == null || a3d.animationClips.length == 0) { + animationClip = new AnimationClip(); + for each (resTrack in parsedTracks) { + animationClip.addTrack(resTrack); + } + animations.push(animationClip); + } else { + for each (var a3DAnim:A3D2AnimationClip in a3d.animationClips) { + animationClip = new AnimationClip(a3DAnim.name); + animationClip.loop = a3DAnim.loop; + for each (var trackID:int in a3DAnim.tracks) { + var track:Track = parsedTracks[trackID]; + if (track != null) { + animationClip.addTrack(track); + } + } + animations.push(animationClip); + } + } + } + + + for each (a3DVertexBuffer in a3d.vertexBuffers) { + a3DVertexBuffers[a3DVertexBuffer.id] = a3DVertexBuffer; + } + + for each (a3DBox in a3d.boxes) { + a3DBoxes[a3DBox.id] = a3DBox; + } + + for each (a3DMaterial in a3d.materials) { + a3DMaterials[a3DMaterial.id] = a3DMaterial; + } + + for each (a3DMap in a3d.maps) { + a3DMaps[a3DMap.id] = a3DMap; + } + + for each (a3DCubeMap in a3d.cubeMaps) { + a3DCubeMaps[a3DCubeMap.id] = a3DCubeMap; + } + + + for each (a3DImage in a3d.images) { + a3DImages[a3DImage.id] = a3DImage; + } + var jointsMap:Dictionary = new Dictionary(); + + for each (a3DJoint in a3d.joints) { + var resJoint:Joint = new Joint(); + resJoint.visible = a3DJoint.visible; + resJoint.name = a3DJoint.name; + parents[resJoint] = a3DJoint.parentId; + jointsMap[a3DJoint.id] = resJoint; + decomposeTransformation(a3DJoint.transform, resJoint); + a3DBox = a3DBoxes[a3DJoint.boundBoxId]; + if (a3DBox != null) { + parseBoundBox(a3DBox.box, resJoint); + } + } + + for each (a3DObject in a3d.objects) { + var resObject:Object3D = new Object3D(); + resObject.visible = a3DObject.visible; + resObject.name = a3DObject.name; + parents[resObject] = a3DObject.parentId; + objectsMap[a3DObject.id] = resObject; + jointsMap[a3DObject.id] = resObject; + decomposeTransformation(a3DObject.transform, resObject); + + a3DBox = a3DBoxes[a3DObject.boundBoxId]; + if (a3DBox != null) { + parseBoundBox(a3DBox.box, resObject); + } + + } + + for each (a3DSprite in a3d.sprites) { + var resSprite:Sprite3D = new Sprite3D(a3DSprite.width, a3DSprite.height); + resSprite.material = parseMaterial(a3DMaterials[a3DSprite.materialId], a3DMaps, a3DCubeMaps, a3DImages); + resSprite.originX = a3DSprite.originX; + resSprite.originY = a3DSprite.originY; + resSprite.perspectiveScale = a3DSprite.perspectiveScale; + resSprite.alwaysOnTop = a3DSprite.alwaysOnTop; + resSprite.rotation = a3DSprite.rotation; + objectsMap[a3DSprite.id] = resSprite; + decomposeTransformation(a3DSprite.transform, resSprite); + } + + for each (a3DSkin in a3d.skins) { + var resSkin:Mesh = parseSkin(a3DSkin, jointsMap, parents, a3DIndexBuffers, a3DVertexBuffers, a3DMaterials, a3DMaps, a3DCubeMaps, a3DImages); + resSkin.visible = a3DSkin.visible; + resSkin.name = a3DSkin.name; + objectsMap[a3DSkin.id] = resSkin; + //The transformation should not affect skin (Due collada comatibility) + //decomposeTransformation(a3DSkin.transform, resSkin); + a3DBox = a3DBoxes[a3DSkin.boundBoxId]; + if (a3DBox != null) { + parseBoundBox(a3DBox.box, resSkin); + } + } + + for each (a3DAmbientLight in a3d.ambientLights) { + var resAmbientLight:AmbientLight = new AmbientLight(a3DAmbientLight.color); + resAmbientLight.intensity = a3DAmbientLight.intensity; + resAmbientLight.visible = a3DAmbientLight.visible; + resAmbientLight.name = a3DAmbientLight.name; + parents[resAmbientLight] = a3DAmbientLight.parentId; + objectsMap[a3DAmbientLight.id] = resAmbientLight; + decomposeTransformation(a3DAmbientLight.transform, resAmbientLight); + a3DBox = a3DBoxes[a3DAmbientLight.boundBoxId]; + if (a3DBox != null) { + parseBoundBox(a3DBox.box, resAmbientLight); + } + } + + for each (a3DOmniLight in a3d.omniLights) { + var resOmniLight:OmniLight = new OmniLight(a3DOmniLight.color, a3DOmniLight.attenuationBegin, a3DOmniLight.attenuationEnd); + resOmniLight.intensity = a3DOmniLight.intensity; + resOmniLight.visible = a3DOmniLight.visible; + resOmniLight.name = a3DOmniLight.name; + parents[resOmniLight] = a3DOmniLight.parentId; + objectsMap[a3DOmniLight.id] = resOmniLight; + decomposeTransformation(a3DOmniLight.transform, resOmniLight); + } + + for each (a3DSpotLight in a3d.spotLights) { + var resSpotLight:SpotLight = new SpotLight(a3DSpotLight.color, a3DSpotLight.attenuationBegin, a3DSpotLight.attenuationEnd, a3DSpotLight.hotspot, a3DSpotLight.falloff); + resSpotLight.intensity = a3DOmniLight.intensity; + resSpotLight.visible = a3DSpotLight.visible; + resSpotLight.name = a3DSpotLight.name; + parents[resSpotLight] = a3DSpotLight.parentId; + objectsMap[a3DSpotLight.id] = resSpotLight; + decomposeTransformation(a3DSpotLight.transform, resSpotLight); + } + + for each(a3DDirLight in a3d.directionalLights) { + var resDirLight:DirectionalLight = new DirectionalLight(a3DDirLight.color); + resDirLight.intensity = resDirLight.intensity; + resDirLight.visible = a3DDirLight.visible; + resDirLight.name = a3DDirLight.name; + parents[resDirLight] = a3DDirLight.parentId; + objectsMap[a3DDirLight.id] = resDirLight; + decomposeTransformation(a3DDirLight.transform, resDirLight); + } + + for each (a3DMesh in a3d.meshes) { + var resMesh:Mesh = parseMesh(a3DMesh, a3DIndexBuffers, a3DVertexBuffers, a3DMaterials, a3DMaps, a3DCubeMaps, a3DImages); + resMesh.visible = a3DMesh.visible; + resMesh.name = a3DMesh.name; + parents[resMesh] = a3DMesh.parentId; + objectsMap[a3DMesh.id] = resMesh; + decomposeTransformation(a3DMesh.transform, resMesh); + a3DBox = a3DBoxes[a3DMesh.boundBoxId]; + if (a3DBox != null) { + parseBoundBox(a3DBox.box, resMesh); + } + } + maps = null; + parsedMaterials = null; + parsedGeometries = null; + } + + private function completeHierarchy():void { + var parent:Long; + var p:Object3D; + var object:Object3D; + for each (object in objectsMap) { + objects.push(object); + if (object.parent != null) continue; + parent = parents[object]; + if (parent != null) { + p = objectsMap[parent]; + if (p != null) { + p.addChild(object); + } else { + hierarchy.push(object); + } + } else { + hierarchy.push(object); + } + } + } + + private function parseBoundBox(box:Vector., destination:Object3D):void { + destination.boundBox = new BoundBox(); + destination.boundBox.minX = box[0]; + destination.boundBox.minY = box[1]; + destination.boundBox.minZ = box[2]; + destination.boundBox.maxX = box[3]; + destination.boundBox.maxY = box[4]; + destination.boundBox.maxZ = box[5]; + } + + private final function unpackVertexBuffer(buffer:ByteArray):void { + var tempBuffer:ByteArray = new ByteArray(); + tempBuffer.endian = Endian.LITTLE_ENDIAN; + buffer.position = 0; + while (buffer.bytesAvailable > 0) { + var data:uint = buffer.readUnsignedShort(); + var vi:uint = data; + vi &= 0x7FFF; + vi ^= (vi + 0x1c000) ^ vi; + vi = vi << 13; + tempBuffer.writeUnsignedInt(data > 0x8000 ? vi | 0x80000000 : vi); + } + buffer.position = 0; + buffer.writeBytes(tempBuffer); + + } + + private function getMatrix3D(transform:A3D2Transform):Matrix3D { + if (transform == null) return null; + var matrix:A3DMatrix = transform.matrix; + return new Matrix3D(Vector.( + [matrix.a, matrix.e, matrix.i, 0, + matrix.b, matrix.f, matrix.j, 0, + matrix.c, matrix.g, matrix.k, 0, + matrix.d, matrix.h, matrix.l, 1 + ])); + } + + private function decomposeTransformation(transform:A3D2Transform, obj:Object3D):void { + if (transform == null) return; + var mat:Matrix3D = getMatrix3D(transform); + var vecs:Vector. = mat.decompose(); + obj.x = vecs[0].x; + obj.y = vecs[0].y; + obj.z = vecs[0].z; + obj.rotationX = vecs[1].x; + obj.rotationY = vecs[1].y; + obj.rotationZ = vecs[1].z; + obj.scaleX = vecs[2].x; + obj.scaleY = vecs[2].y; + obj.scaleZ = vecs[2].z; + } + + private function decomposeBindTransformation(transform:A3D2Transform, obj:Joint):void { + if (transform == null) return; + var matrix:A3DMatrix = transform.matrix; + var mat:Vector. = Vector.([ + matrix.a, matrix.b, matrix.c, matrix.d, + matrix.e, matrix.f, matrix.g, matrix.h, + matrix.i, matrix.j, matrix.k, matrix.l] + ); + + obj.setBindPoseMatrix(mat); + } + + private function parseMesh(a3DMesh:A3D2Mesh, indexBuffers:Dictionary, vertexBuffers:Dictionary, materials:Dictionary, a3DMaps:Dictionary, a3DCubeMaps:Dictionary, images:Dictionary):Mesh { + var res:Mesh = new Mesh(); + res.geometry = parseGeometry(a3DMesh.indexBufferId, a3DMesh.vertexBuffers, indexBuffers, vertexBuffers); + var surfaces:Vector. = a3DMesh.surfaces; + for (var i:int = 0; i < surfaces.length; i++) { + var s:A3D2Surface = surfaces[i]; + var m:ParserMaterial = parseMaterial(materials[s.materialId], a3DMaps, a3DCubeMaps, images); + res.addSurface(m, s.indexBegin, s.numTriangles); + } + return res; + } + + private function parseSkin(a3DSkin:A3D2Skin, jointsMap:Dictionary, parents:Dictionary, indexBuffers:Dictionary, vertexBuffers:Dictionary, materials:Dictionary, a3DMaps:Dictionary, a3DCubeMaps:Dictionary, images:Dictionary):Skin { + var geometry:Geometry = parseGeometry(a3DSkin.indexBufferId, a3DSkin.vertexBuffers, indexBuffers, vertexBuffers); + var res:Skin = new Skin(getNumInfluences(geometry)); + res.geometry = geometry; + var surfaces:Vector. = a3DSkin.surfaces; + for (var i:int = 0; i < surfaces.length; i++) { + var s:A3D2Surface = surfaces[i]; + var m:ParserMaterial = parseMaterial(materials[s.materialId], a3DMaps, a3DCubeMaps, images); + res.addSurface(m, s.indexBegin, s.numTriangles); + } + copyBones(res, a3DSkin, jointsMap, parents); + return res; + } + + private function copyBones(skin:Skin, a3DSkin:A3D2Skin, jointsMap:Dictionary, parents:Dictionary):void { + var rootBones:Vector. = new Vector.(); + var s2dMap:Dictionary = new Dictionary(); + var sourceJoints:Dictionary = new Dictionary(); + var jointIDs:Dictionary = new Dictionary(); + var joint:Joint; + var object:Object3D; + var indexOffset:uint = 0; + var dJoint:Joint; + for each (var numJoints:uint in a3DSkin.numJoints) { + for (var i:int = 0; i < numJoints; i++) { + var key:Long = a3DSkin.joints[int(indexOffset + i)]; + object = jointsMap[key]; + sourceJoints[key] = object; + jointIDs[object] = key; + } + indexOffset += numJoints; + } + + for (var idk:* in sourceJoints) { + object = sourceJoints[idk]; + if (object == null) { + throw new Error("Joint for skin " + a3DSkin.name + " not found"); + } + delete objectsMap[idk]; + s2dMap[object] = cloneJoint(object); + } + var count:int; + indexOffset = 0; + for (i = 0, count = a3DSkin.numJoints.length; i < count; i++) { + numJoints = a3DSkin.numJoints[i]; + skin.surfaceJoints[i] = new Vector.(); + for (var j:int = 0; j < numJoints; j++) { + skin.surfaceJoints[i].push(s2dMap[sourceJoints[a3DSkin.joints[int(indexOffset + j)]]]); + } + indexOffset += numJoints; + } + skin.calculateSurfacesProcedures(); + + for (i = 0; i < a3DSkin.jointBindTransforms.length; i++) { + var bindPose:A3D2JointBindTransform = a3DSkin.jointBindTransforms[i]; + //Joint is not affect to vertices, but affect on transformation of other joints (due to hierarchy). + if (sourceJoints[bindPose.id] == null) { + object = jointsMap[bindPose.id]; + sourceJoints[bindPose.id] = object; + s2dMap[object] = cloneJoint(object); + } + decomposeBindTransformation(bindPose.bindPoseTransform, Joint(s2dMap[sourceJoints[bindPose.id]])); + } + var skinParent:Long = null; + for each(object in sourceJoints) { + dJoint = s2dMap[object]; + var parent:Long = parents[object]; + if (isRootJointNode(object, parents, sourceJoints, jointsMap)) { + skinParent = parent; + rootBones.push(dJoint); + } else { + var pJointSource:Object3D = jointsMap[parent]; + var pJoint:Joint = s2dMap[pJointSource]; + if (pJoint == null) { + attachJoint(dJoint, object, parents, jointsMap, s2dMap); + } else { + pJoint.addChild(dJoint); + } + } + } + if (skinParent != null) { + parents[skin] = skinParent; + } + + skin._renderedJoints = new Vector.(); + for (i = 0; i < numJoints; i++) { + skin._renderedJoints.push(s2dMap[sourceJoints[a3DSkin.joints[i]]]); + } + + for each(joint in rootBones) { + skin.addChild(joint); + } + } + + private function attachJoint(joint:Joint, source:Object3D, parents:Dictionary, sourceJoints:Dictionary, s2dMap:Dictionary):void { + var parentID:Long = parents[source]; + var parentSource:Object3D = sourceJoints[parentID]; + var parentDestination:Joint = s2dMap[parentSource]; + if (parentDestination == null) { + s2dMap[parentSource] = parentDestination = cloneJoint(parentSource); + delete objectsMap[parentID]; + attachJoint(parentDestination, parentSource, parents, sourceJoints, s2dMap); + } + parentDestination.addChild(joint); + } + + private function isRootJointNode(joint:Object3D, parents:Dictionary, joints:Dictionary, jointsMap:Dictionary):Boolean { + var parent:Long = parents[joint]; + while (parent != null) { + var current:Object3D = jointsMap[parent]; + if (joints[parent] != null) { + return false; + } + parent = parents[current]; + } + + return true; + } + + private function cloneJoint(source:Object3D):Joint { + var result:Joint = new Joint(); + result.name = source.name; + result.visible = source.visible; + result.boundBox = source.boundBox ? source.boundBox.clone() : null; + result._x = source._x; + result._y = source._y; + result._z = source._z; + result._rotationX = source._rotationX; + result._rotationY = source._rotationY; + result._rotationZ = source._rotationZ; + result._scaleX = source._scaleX; + result._scaleY = source._scaleY; + result._scaleZ = source._scaleZ; + result.composeTransforms(); + return result; + } + + private function getNumInfluences(geometry:Geometry):uint { + var result:uint = 0; + for (var i:int = 0, count:int = VertexAttributes.JOINTS.length; i < count; i++) { + if (geometry.hasAttribute(VertexAttributes.JOINTS[i])) { + result += 2; + } + } + return result; + } + + private function parseGeometry(indexBufferID:int, vertexBuffersIDs:Vector., indexBuffers:Dictionary, vertexBuffers:Dictionary):Geometry { + var key:String = "i" + indexBufferID.toString(); + for each(var id:int in vertexBuffersIDs) { + key += "v" + id.toString(); + } + var geometry:Geometry = parsedGeometries[key]; + if (geometry != null) return geometry; + geometry = new Geometry(); + var a3dIB:A3D2IndexBuffer = indexBuffers[indexBufferID]; + + var indices:Vector. = A3DUtils.byteArrayToVectorUint(a3dIB.byteBuffer); + var uvoffset:int = 0; + geometry._indices = indices; + var buffers:Vector. = vertexBuffersIDs; + var vertexCount:uint; + for (var j:int = 0; j < buffers.length; j++) { + var buffer:A3D2VertexBuffer = vertexBuffers[buffers[j]]; + if (compressedBuffers) { + if (unpackedBuffers[buffer] == null) { + unpackVertexBuffer(buffer.byteBuffer); + unpackedBuffers[buffer] = true; + } + } + + vertexCount = buffer.vertexCount; + var byteArray:ByteArray = buffer.byteBuffer; + byteArray.endian = Endian.LITTLE_ENDIAN; + var offset:int = 0; + var attributes:Array = new Array(); + var jointsOffset:int = 0; + for (var k:int = 0; k < buffer.attributes.length; k++) { + var attr:int; + switch (buffer.attributes[k]) { + case A3D2VertexAttributes.POSITION: + attr = VertexAttributes.POSITION; + break; + case A3D2VertexAttributes.NORMAL: + attr = VertexAttributes.NORMAL; + break; + case A3D2VertexAttributes.TANGENT4: + attr = VertexAttributes.TANGENT4; + break; + case A3D2VertexAttributes.TEXCOORD: + attr = VertexAttributes.TEXCOORDS[uvoffset]; + uvoffset++; + break; + case A3D2VertexAttributes.JOINT: + attr = VertexAttributes.JOINTS[jointsOffset]; + jointsOffset++; + break; + } + var numFloats:int = VertexAttributes.getAttributeStride(attr); + numFloats = (numFloats < 1) ? 1 : numFloats; + for (var t:int = 0; t < numFloats; t++) { + attributes[offset] = attr; + offset++; + } + } + geometry.addVertexStream(attributes); + geometry._vertexStreams[0].data = byteArray; + } + geometry._numVertices = (buffers.length > 0) ? vertexCount : 0; + parsedGeometries[key] = geometry; + + return geometry; + } + + private function parseMap(source:A3D2Map, images:Dictionary):ExternalTextureResource { + if (source == null) return null; + var res:ExternalTextureResource = maps[source.imageId]; + if (res != null) return res; + res = maps[source.imageId] = new ExternalTextureResource(images[source.imageId].url); + return res; + } + + private function parseCubeMap(source:A3D2CubeMap, images:Dictionary):ExternalTextureResource { + return null; + } + + private function parseMaterial(source:A3D2Material, a3DMaps:Dictionary, a3DCubeMaps:Dictionary, images:Dictionary):ParserMaterial { + if (source == null) return null; + var res:ParserMaterial = parsedMaterials[source.id]; + if (res != null) return res; + + res = parsedMaterials[source.id] = new ParserMaterial(); + res.textures["diffuse"] = parseMap(a3DMaps[source.diffuseMapId], images); + res.textures["emission"] = parseMap(a3DMaps[source.lightMapId], images); + res.textures["bump"] = parseMap(a3DMaps[source.normalMapId], images); + res.textures["specular"] = parseMap(a3DMaps[source.specularMapId], images); + res.textures["glossiness"] = parseMap(a3DMaps[source.glossinessMapId], images); + res.textures["transparent"] = parseMap(a3DMaps[source.opacityMapId], images); + res.textures["reflection"] = parseCubeMap(a3DCubeMaps[source.reflectionCubeMapId], images); + materials.push(res); + return res; + } + + private static function convert1_2(source:A3D):A3D2 { + // source.boxes + var sourceBoxes:Vector. = source.boxes; + var destBoxes:Vector. = null; + if (sourceBoxes != null) { + destBoxes = new Vector.(); + for (var i:int = 0, count:int = sourceBoxes.length; i < count; i++) { + var sourceBox:A3DBox = sourceBoxes[i]; + var destBox:A3D2Box = new A3D2Box(sourceBox.box, sourceBox.id.id); + destBoxes[i] = destBox; + } + } + + // source.geometries + var sourceGeometries:Dictionary = new Dictionary(); + if (source.geometries != null) { + for each(var sourceGeometry:A3DGeometry in source.geometries) { + sourceGeometries[sourceGeometry.id.id] = sourceGeometry; + } + } + + // source.images + var sourceImages:Vector. = source.images; + var destImages:Vector. = null; + if (sourceImages != null) { + destImages = new Vector.(); + for (i = 0, count = sourceImages.length; i < count; i++) { + var sourceImage:A3DImage = sourceImages[i]; + var destImage:A3D2Image = new A3D2Image(sourceImage.id.id, sourceImage.url); + destImages[i] = destImage; + } + } + + // source.maps + var sourceMaps:Vector. = source.maps; + var destMaps:Vector. = null; + if (sourceMaps != null) { + destMaps = new Vector.(); + for (i = 0, count = sourceMaps.length; i < count; i++) { + var sourceMap:A3DMap = sourceMaps[i]; + var destMap:A3D2Map = new A3D2Map(sourceMap.channel, sourceMap.id.id, sourceMap.imageId.id); + destMaps[i] = destMap; + } + } + + // source.materials + var sourceMaterials:Vector. = source.materials; + var destMaterials:Vector. = null; + if (sourceMaterials != null) { + destMaterials = new Vector.(); + for (i = 0, count = sourceMaterials.length; i < count; i++) { + var sourceMaterial:A3DMaterial = sourceMaterials[i]; + var destMaterial:A3D2Material = + new A3D2Material( + idToInt(sourceMaterial.diffuseMapId), + idToInt(sourceMaterial.glossinessMapId), + idToInt(sourceMaterial.id), + idToInt(sourceMaterial.lightMapId), + idToInt(sourceMaterial.normalMapId), + idToInt(sourceMaterial.opacityMapId), + -1, + idToInt(sourceMaterial.specularMapId) + ); + destMaterials[i] = destMaterial; + } + } + + // source.objects + var sourceObjects:Vector. = source.objects; + var destObjects:Vector. = null; + var destMeshes:Vector. = null; + var destVertexBuffers:Vector. = null; + var destIndexBuffers:Vector. = null; + var lastIndexBufferIndex:uint = 0; + var lastVertexBufferIndex:uint = 0; + var objectsMap:Dictionary = new Dictionary(); + if (sourceObjects != null) { + destMeshes = new Vector.(); + destObjects = new Vector.(); + destVertexBuffers = new Vector.(); + destIndexBuffers = new Vector.(); + for (i = 0, count = sourceObjects.length; i < count; i++) { + var sourceObject:A3DObject = sourceObjects[i]; + if (sourceObject.surfaces != null && sourceObject.surfaces.length > 0) { + var destMesh:A3D2Mesh = null; + sourceGeometry = sourceGeometries[sourceObject.geometryId.id]; + var destIndexBufferId:int = -1; + var destVertexBuffersIds:Vector. = new Vector.(); + if (sourceGeometry != null) { + + var sourceIndexBuffer:A3DIndexBuffer = sourceGeometry.indexBuffer; + var sourceVertexBuffers:Vector. = sourceGeometry.vertexBuffers; + var destIndexBuffer:A3D2IndexBuffer = new A3D2IndexBuffer(sourceIndexBuffer.byteBuffer, lastIndexBufferIndex++, sourceIndexBuffer.indexCount); + destIndexBufferId = destIndexBuffer.id; + destIndexBuffers.push(destIndexBuffer); + for (var j:int = 0, inCount:int = sourceVertexBuffers.length; j < inCount; j++) { + var sourceVertexBuffer:A3DVertexBuffer = sourceVertexBuffers[j]; + var sourceAttributes:Vector. = sourceVertexBuffer.attributes; + var destAttributes:Vector. = new Vector.(); + for (var k:int = 0, kCount:int = sourceAttributes.length; k < kCount; k++) { + var attr:int = sourceAttributes[k]; + + switch (attr) { + case 0: + destAttributes[k] = A3D2VertexAttributes.POSITION; + break; + case 1: + destAttributes[k] = A3D2VertexAttributes.NORMAL; + break; + case 2: + destAttributes[k] = A3D2VertexAttributes.TANGENT4; + break; + case 3: + break; + case 4: + break; + case 5: + destAttributes[k] = A3D2VertexAttributes.TEXCOORD; + break; + } + } + var destVertexBuffer:A3D2VertexBuffer = + new A3D2VertexBuffer( + destAttributes, + sourceVertexBuffer.byteBuffer, + lastVertexBufferIndex++, + sourceVertexBuffer.vertexCount + ); + destVertexBuffers.push(destVertexBuffer); + destVertexBuffersIds.push(destVertexBuffer.id); + } + } + destMesh = new A3D2Mesh( + idToInt(sourceObject.boundBoxId), + idToLong(sourceObject.id), + destIndexBufferId, + sourceObject.name, + convertParent1_2(sourceObject.parentId), + convertSurfaces1_2(sourceObject.surfaces), + new A3D2Transform(sourceObject.transformation.matrix), + destVertexBuffersIds, + sourceObject.visible + ); + destMeshes.push(destMesh); + objectsMap[sourceObject.id.id] = destMesh; + } else { + var destObject:A3D2Object = new A3D2Object( + idToInt(sourceObject.boundBoxId), + idToLong(sourceObject.id), + sourceObject.name, + convertParent1_2(sourceObject.parentId), + new A3D2Transform(sourceObject.transformation.matrix), + sourceObject.visible + ); + destObjects.push(destObject); + objectsMap[sourceObject.id.id] = destObject; + } + } + } + + var result:A3D2 = new A3D2( + null, null, null, destBoxes, null, null, null, destImages, destIndexBuffers, null, + destMaps, destMaterials, + destMeshes != null && destMeshes.length > 0 ? destMeshes : null, + destObjects != null && destObjects.length > 0 ? destObjects : null, + null, null, null, null, destVertexBuffers + ); + return result; + } + + private static function idToInt(id:Id):int { + return id != null ? id.id : -1; + } + + private static function idToLong(id:Id):Long { + return id != null ? Long.fromInt(id.id) : Long.fromInt(-1); + } + + private static function convertParent1_2(parentId:ParentId):Long { + if (parentId == null) return null; + return parentId != null ? Long.fromInt(parentId.id) : null; + } + + private static function convertSurfaces1_2(source:Vector.):Vector. { + var dest:Vector. = new Vector.(); + for (var i:int = 0, count:int = source.length; i < count; i++) { + var sourceSurface:A3DSurface = source[i]; + var destSurface:A3D2Surface = new A3D2Surface( + sourceSurface.indexBegin, + idToInt(sourceSurface.materialId), + sourceSurface.numTriangles); + dest[i] = destSurface; + } + return dest; + } + + alternativa3d static function traceGeometry(geometry:Geometry):void { + var vertexStream:VertexStream = geometry._vertexStreams[0]; + var prev:int = -1; + + var attribtuesLength:int = vertexStream.attributes.length; + var stride:int = attribtuesLength*4; + var length:int = vertexStream.data.length/stride; + var data:ByteArray = vertexStream.data; + + for (var j:int = 0; j < length; j++) { + var traceString:String = "V" + j + " "; + var offset:int = -4; + for (var i:int = 0; i < attribtuesLength; i++) { + var attr:int = vertexStream.attributes[i]; + var x:Number, y:Number, z:Number; + if (attr == prev) continue; + offset = geometry.getAttributeOffset(attr)*4; + switch (attr) { + case VertexAttributes.POSITION: + data.position = j*stride + offset; + traceString += "P[" + data.readFloat().toFixed(2) + ", " + data.readFloat().toFixed(2) + ", " + data.readFloat().toFixed(2) + "] "; + break; + case 20: + data.position = j*stride + offset; + traceString += "A[" + data.readFloat().toString(2) + "]"; + break; + case VertexAttributes.NORMAL: + data.position = j*stride + offset; + x = data.readFloat(); + y = data.readFloat(); + z = data.readFloat(); + break; + case VertexAttributes.TANGENT4: + data.position = j*stride + offset; + x = data.readFloat(); + y = data.readFloat(); + z = data.readFloat(); + break; + case VertexAttributes.JOINTS[0]: + data.position = j*stride + offset; + traceString += "J0[" + data.readFloat().toFixed(0) + " = " + data.readFloat().toFixed(2) + ", " + data.readFloat().toFixed(0) + " = " + data.readFloat().toFixed(2) + "] "; + break; + case VertexAttributes.JOINTS[1]: + data.position = j*stride + offset; + traceString += "J1[" + data.readFloat().toFixed(0) + " = " + data.readFloat().toFixed(2) + ", " + data.readFloat().toFixed(0) + " = " + data.readFloat().toFixed(2) + "] "; + break; + case VertexAttributes.JOINTS[2]: + data.position = j*stride + offset; + traceString += "J1[" + data.readFloat().toFixed(0) + " = " + data.readFloat().toFixed(2) + ", " + data.readFloat().toFixed(0) + " = " + data.readFloat().toFixed(2) + "] "; + break; + case VertexAttributes.JOINTS[3]: + data.position = j*stride + offset; + traceString += "J1[" + data.readFloat().toFixed(0) + " = " + data.readFloat().toFixed(2) + ", " + data.readFloat().toFixed(0) + " = " + data.readFloat().toFixed(2) + "] "; + break; + + } + prev = attr; + } + trace(traceString); + + } + + } + } +} diff --git a/src/alternativa/engine3d/loaders/Parser3DS.as b/src/alternativa/engine3d/loaders/Parser3DS.as new file mode 100644 index 0000000..2badead --- /dev/null +++ b/src/alternativa/engine3d/loaders/Parser3DS.as @@ -0,0 +1,1499 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.lights.OmniLight; + import alternativa.engine3d.lights.SpotLight; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.resources.ExternalTextureResource; + import alternativa.engine3d.resources.Geometry; + + import flash.geom.Matrix; + import flash.geom.Matrix3D; + import flash.geom.Vector3D; + import flash.utils.ByteArray; + import flash.utils.Endian; + + use namespace alternativa3d; + + /** + * Parser of .3ds files , that are presented as ByteArray. + */ + public class Parser3DS extends Parser { + + private static const CHUNK_MAIN:int = 0x4D4D; + private static const CHUNK_VERSION:int = 0x0002; + private static const CHUNK_SCENE:int = 0x3D3D; + private static const CHUNK_ANIMATION:int = 0xB000; + private static const CHUNK_OBJECT:int = 0x4000; + private static const CHUNK_TRIMESH:int = 0x4100; + private static const CHUNK_LIGHT:int = 0x4600; + private static const CHUNK_CAMERA:int = 0x4700; + private static const CHUNK_VERTICES:int = 0x4110; + private static const CHUNK_FACES:int = 0x4120; + private static const CHUNK_FACESMATERIAL:int = 0x4130; + private static const CHUNK_FACESSMOOTHGROUPS:int = 0x4150; + private static const CHUNK_MAPPINGCOORDS:int = 0x4140; + private static const CHUNK_TRANSFORMATION:int = 0x4160; + private static const CHUNK_MATERIAL:int = 0xAFFF; + + private var data:ByteArray; + private var objectDatas:Object; + private var animationDatas:Array; + private var materialDatas:Object; + + /** + * Performs parsing. + * Result of parsing is placed in lists are follows objects, parents, materials. + * @param data ByteArray correspond to content of a 3ds file. + * @param texturesBaseURL Base path to texture files. After parsing diffuseMapURL and opacityMapURL properties gets string values, that consists of texturesBaseURL and file name. + * @param scale Amount to multiply vertex coordinates, objects coordinates and values of objects scaling. + * @param respectSmoothGroups Flag of accounting of smoothing groups. If flag set to true, then all vertices will duplicated according to smoothing groups, specified for the objects. + * + * @see alternativa.engine3d.loaders.ParserMaterial + * @see #objects + * @see #hierarchy + * @see #materials + */ + + public function parse(data:ByteArray, texturesBaseURL:String = "", scale:Number = 1, respectSmoothGroups:Boolean = false):void { + if (data.bytesAvailable < 6) return; + this.data = data; + data.endian = Endian.LITTLE_ENDIAN; + parse3DSChunk(data.position, data.bytesAvailable); + objects = new Vector.(); + hierarchy = new Vector.(); + materials = new Vector.(); + buildContent(texturesBaseURL, scale, respectSmoothGroups); + this.data = null; + objectDatas = null; + animationDatas = null; + materialDatas = null; + } + + private function readChunkInfo(dataPosition:int):ChunkInfo { + data.position = dataPosition; + var chunkInfo:ChunkInfo = new ChunkInfo(); + chunkInfo.id = data.readUnsignedShort(); + chunkInfo.size = data.readUnsignedInt(); + chunkInfo.dataSize = chunkInfo.size - 6; + chunkInfo.dataPosition = data.position; + chunkInfo.nextChunkPosition = dataPosition + chunkInfo.size; + return chunkInfo; + } + + private function parse3DSChunk(dataPosition:int, bytesAvailable:int):void { + if (bytesAvailable < 6) return; + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + data.position = dataPosition; + switch (chunkInfo.id) { + // Main + case CHUNK_MAIN: + parseMainChunk(chunkInfo.dataPosition, chunkInfo.dataSize); + break; + } + parse3DSChunk(chunkInfo.nextChunkPosition, bytesAvailable - chunkInfo.size); + } + + private function parseMainChunk(dataPosition:int, bytesAvailable:int):void { + if (bytesAvailable < 6) return; + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + switch (chunkInfo.id) { + // Version + case CHUNK_VERSION: + //version = data.readUnsignedInt(); + break; + // 3D-scene + case CHUNK_SCENE: + parse3DChunk(chunkInfo.dataPosition, chunkInfo.dataSize); + break; + // Animation + case CHUNK_ANIMATION: + parseAnimationChunk(chunkInfo.dataPosition, chunkInfo.dataSize); + break; + } + parseMainChunk(chunkInfo.nextChunkPosition, bytesAvailable - chunkInfo.size); + } + + private function parse3DChunk(dataPosition:int, bytesAvailable:int):void { + while (bytesAvailable >= 6) { + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + switch (chunkInfo.id) { + // Material + case CHUNK_MATERIAL: + // Parse material + var material:MaterialData = new MaterialData(); + parseMaterialChunk(material, chunkInfo.dataPosition, chunkInfo.dataSize); + break; + // Object + case CHUNK_OBJECT: + parseObject(chunkInfo); + break; + } + dataPosition = chunkInfo.nextChunkPosition; + bytesAvailable -= chunkInfo.size; + } + } + + private function parseObject(chunkInfo:ChunkInfo):void { + // Create list of objects, if it need. + if (objectDatas == null) { + objectDatas = new Object(); + } + // Create object data + var object:ObjectData = new ObjectData(); + // Get object name + object.name = getString(chunkInfo.dataPosition); + // Get object data to list + objectDatas[object.name] = object; + // Parse object + var offset:int = object.name.length + 1; + parseObjectChunk(object, chunkInfo.dataPosition + offset, chunkInfo.dataSize - offset); + } + + private function parseObjectChunk(object:ObjectData, dataPosition:int, bytesAvailable:int):void { + if (bytesAvailable < 6) return; + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + switch (chunkInfo.id) { + // Mesh + case CHUNK_TRIMESH: + parseMeshChunk(object, chunkInfo.dataPosition, chunkInfo.dataSize); + break; + // Light source + case CHUNK_LIGHT: + parseLightChunk(object, chunkInfo.dataPosition, chunkInfo.dataSize); + break; + // Camera + case CHUNK_CAMERA: + parseCameraChunk(object, chunkInfo.dataSize); + break; + } + parseObjectChunk(object, chunkInfo.nextChunkPosition, bytesAvailable - chunkInfo.size); + } + + private function parseMeshChunk(object:ObjectData, dataPosition:int, bytesAvailable:int):void { + if (bytesAvailable < 6) return; + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + switch (chunkInfo.id) { + // Vertices + case CHUNK_VERTICES: + parseVertices(object); + break; + // UV + case CHUNK_MAPPINGCOORDS: + parseUVs(object); + break; + // Transformation + case CHUNK_TRANSFORMATION: + parseMatrix(object); + break; + // Faces + case CHUNK_FACES: + parseFaces(object, chunkInfo); + break; + } + parseMeshChunk(object, chunkInfo.nextChunkPosition, bytesAvailable - chunkInfo.size); + } + + private function parseVertices(object:ObjectData):void { + var num:int = data.readUnsignedShort(); + object.vertices = new Vector.(3*num, true); + for (var i:int = 0, j:int = 0; i < num; i++) { + object.vertices[j++] = data.readFloat(); + object.vertices[j++] = data.readFloat(); + object.vertices[j++] = data.readFloat(); + } + } + + private function parseUVs(object:ObjectData):void { + var num:int = data.readUnsignedShort(); + object.uvs = new Vector.(2*num, true); + for (var i:int = 0, j:int = 0; i < num; i++) { + object.uvs[j++] = data.readFloat(); + object.uvs[j++] = data.readFloat(); + } + } + + private function parseMatrix(object:ObjectData):void { + object.a = data.readFloat(); + object.e = data.readFloat(); + object.i = data.readFloat(); + object.b = data.readFloat(); + object.f = data.readFloat(); + object.j = data.readFloat(); + object.c = data.readFloat(); + object.g = data.readFloat(); + object.k = data.readFloat(); + object.d = data.readFloat(); + object.h = data.readFloat(); + object.l = data.readFloat(); + } + + private function parseFaces(object:ObjectData, chunkInfo:ChunkInfo):void { + var num:int = data.readUnsignedShort(); + object.smoothGroups = new Vector.(num, true); + object.faces = new Vector.(3*num, true); + for (var i:int = 0, j:int = 0; i < num; i++) { + object.faces[j++] = data.readUnsignedShort(); + object.faces[j++] = data.readUnsignedShort(); + object.faces[j++] = data.readUnsignedShort(); + data.position += 2; // Skip the flag of edges rendering + } + var offset:int = 2 + 8*num; + parseFacesChunk(object, chunkInfo.dataPosition + offset, chunkInfo.dataSize - offset); + } + + private function parseFacesChunk(object:ObjectData, dataPosition:int, bytesAvailable:int):void { + if (bytesAvailable < 6) return; + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + switch (chunkInfo.id) { + // Surfaces + case CHUNK_FACESMATERIAL: + parseSurface(object); + break; + // Smoothing groups. + case CHUNK_FACESSMOOTHGROUPS: + parseSmoothGroups(object); + break; + } + parseFacesChunk(object, chunkInfo.nextChunkPosition, bytesAvailable - chunkInfo.size); + } + + private function parseSurface(object:ObjectData):void { + // Create list of surfaces, if it need. + if (object.surfaces == null) { + object.surfaces = new Object(); + } + // Name of surface and number of faces. + var sur:String = getString(data.position); + var num:int = data.readUnsignedShort(); + if (num > 0) { + // Create surface data + var surface:Vector. = new Vector.(num + 1); + // Put surface data to list + object.surfaces[sur] = surface; + // Get faces of surface + for (var i:int = 0; i < num; i++) { + surface[i] = data.readUnsignedShort(); + } + // Also stores number of the material (starts from 1) (additionally store the serial number of material (beginning from the one)) + surface[num] = (object.surfacesCount++); + } + } + + private function parseSmoothGroups(object:ObjectData):void { + var num:int = object.faces.length/3; + for (var i:int = 0; i < num; i++) { + object.smoothGroups [i] = data.readUnsignedInt(); + } + } + + private function parseAnimationChunk(dataPosition:int, bytesAvailable:int):void { + while (bytesAvailable >= 6) { + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + switch (chunkInfo.id) { + // Object animation + case 0xB001: // ambient o_O + case 0xB002: + case 0xB003: + case 0xB004: // cam target + case 0xB005: + case 0xB006: // spot target + case 0xB007: + if (animationDatas == null) { + animationDatas = new Array(); + } + var animation:AnimationData = new AnimationData(); + animation.chunkId = chunkInfo.id; + animationDatas.push(animation); + parseObjectAnimationChunk(animation, chunkInfo.dataPosition, chunkInfo.dataSize); + break; + // Timeline + case 0xB008: + break; + } + dataPosition = chunkInfo.nextChunkPosition; + bytesAvailable -= chunkInfo.size; + } + } + + private function parseObjectAnimationChunk(animation:AnimationData, dataPosition:int, bytesAvailable:int):void { + if (bytesAvailable < 6) return; + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + switch (chunkInfo.id) { + // Identification of object and its link + case 0xB010: + // Name of the object + animation.objectName = getString(data.position); + if ((animation.chunkId == 0xB004) || (animation.chunkId == 0xB006)) animation.objectName += "_target"; + data.position += 4; + // Index of parent object in plain list of scene objects. + animation.parentIndex = data.readUnsignedShort(); + break; + // Name of dummy object + case 0xB011: + animation.instanceOf = animation.objectName; + animation.objectName = getString(data.position); + break; + // Pivot + case 0xB013: + animation.pivot = new Vector3D(data.readFloat(), data.readFloat(), data.readFloat()); + break; + // Offset of the object relative to its parent + case 0xB020: + data.position += 20; + animation.position = new Vector3D(data.readFloat(), data.readFloat(), data.readFloat()); + break; + // Rotation of object relative to its parent (angle-axis) + case 0xB021: + data.position += 20; + animation.rotation = getRotationFrom3DSAngleAxis(data.readFloat(), data.readFloat(), data.readFloat(), data.readFloat()); + break; + // Scale of object relative to its parent + case 0xB022: + data.position += 20; + animation.scale = new Vector3D(data.readFloat(), data.readFloat(), data.readFloat()); + break; + } + parseObjectAnimationChunk(animation, chunkInfo.nextChunkPosition, bytesAvailable - chunkInfo.size); + } + + private function parseMaterialChunk(material:MaterialData, dataPosition:int, bytesAvailable:int):void { + if (bytesAvailable < 6) return; + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + switch (chunkInfo.id) { + // Name of material + case 0xA000: + parseMaterialName(material); + break; + // Ambient color + case 0xA010: + data.position = chunkInfo.dataPosition + 6; + material.ambient = (data.readUnsignedByte() << 16) + (data.readUnsignedByte() << 8) + data.readUnsignedByte(); + break; + // Diffuse color + case 0xA020: + data.position = chunkInfo.dataPosition + 6; + material.diffuse = (data.readUnsignedByte() << 16) + (data.readUnsignedByte() << 8) + data.readUnsignedByte(); + break; + // Specular color + case 0xA030: + data.position = chunkInfo.dataPosition + 6; + material.specular = (data.readUnsignedByte() << 16) + (data.readUnsignedByte() << 8) + data.readUnsignedByte(); + break; + // Shininess percent + case 0xA040: + data.position = chunkInfo.dataPosition + 6; + material.glossiness = data.readUnsignedShort(); + break; + // Shininess strength percent + case 0xA041: + break; + // Transparensy + case 0xA050: + data.position = chunkInfo.dataPosition + 6; + material.transparency = data.readUnsignedShort(); + break; + // Texture map 1 + case 0xA200: + parseMaterialMapData("diffuse", material, chunkInfo); + break; + // Texture map 2 + case 0xA33A: + break; + // Opacity map + case 0xA210: + parseMaterialMapData("transparent", material, chunkInfo); + break; + // Bump map + case 0xA230: + parseMaterialMapData("bump", material, chunkInfo); + break; + // Specular map + case 0xA204: + parseMaterialMapData("specular", material, chunkInfo); + break; + // Shininess map + case 0xA33C: + parseMaterialMapData("glossiness", material, chunkInfo); + break; + // Self-illumination map + case 0xA33D: + parseMaterialMapData("emission", material, chunkInfo); + break; + // Reflection map + case 0xA220: + parseMaterialMapData("reflective", material, chunkInfo); + break; + } + parseMaterialChunk(material, chunkInfo.nextChunkPosition, bytesAvailable - chunkInfo.size); + } + + private function parseMaterialMapData(channel:String, material:MaterialData, chunkInfo:ChunkInfo):void { + var map:MapData = new MapData; + map.channel = channel; + parseMapChunk(material.name, map, chunkInfo.dataPosition, chunkInfo.dataSize); + material.maps.push(map); + } + + private function parseMaterialName(material:MaterialData):void { + // Create list of materials, if it need + if (materialDatas == null) { + materialDatas = new Object(); + } + // Get name of material + material.name = getString(data.position); + // Put data of material in list + materialDatas[material.name] = material; + } + + private function parseMapChunk(materialName:String, map:MapData, dataPosition:int, bytesAvailable:int):void { + if (bytesAvailable < 6) return; + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + switch (chunkInfo.id) { + // File name + case 0xA300: + map.filename = getString(chunkInfo.dataPosition).toLowerCase(); + break; + case 0xA351: + // Texture mapping options + break; + // Scale along U + case 0xA354: + map.scaleU = data.readFloat(); + break; + // Scale along V + case 0xA356: + map.scaleV = data.readFloat(); + break; + // Offset along U + case 0xA358: + map.offsetU = data.readFloat(); + break; + // Offset along V + case 0xA35A: + map.offsetV = data.readFloat(); + break; + // Rotation angle + case 0xA35C: + map.rotation = data.readFloat(); + break; + } + parseMapChunk(materialName, map, chunkInfo.nextChunkPosition, bytesAvailable - chunkInfo.size); + } + + private function getString(index:int):String { + data.position = index; + var charCode:int; + var res:String = ""; + while ((charCode = data.readByte()) != 0) { + res += String.fromCharCode(charCode); + } + return res; + } + + private function getRotationFrom3DSAngleAxis(angle:Number, x:Number, z:Number, y:Number):Vector3D { + var res:Vector3D = new Vector3D(); + var s:Number = Math.sin(angle); + var c:Number = Math.cos(angle); + var t:Number = 1 - c; + var k:Number = x*y*t + z*s; + var half:Number; + if (k >= 1) { + half = angle/2; + res.z = -2*Math.atan2(x*Math.sin(half), Math.cos(half)); + res.y = -Math.PI/2; + res.x = 0; + return res; + } + if (k <= -1) { + half = angle/2; + res.z = 2*Math.atan2(x*Math.sin(half), Math.cos(half)); + res.y = Math.PI/2; + res.x = 0; + return res; + } + res.z = -Math.atan2(y*s - x*z*t, 1 - (y*y + z*z)*t); + res.y = -Math.asin(x*y*t + z*s); + res.x = -Math.atan2(x*s - y*z*t, 1 - (x*x + z*z)*t); + return res; + } + + private function parseLightChunk(object:ObjectData, dataPosition:int, bytesAvailable:int):void { + if (bytesAvailable < 6 + 12) return; + var x:Number = data.readFloat(); + var y:Number = data.readFloat(); + var z:Number = data.readFloat(); + object.position = new Vector3D(x, y, z); + parseLightSubChunk(object, dataPosition + 12, bytesAvailable - 12); + } + + private function parseLightSubChunk(object:ObjectData, dataPosition:int, bytesAvailable:int):void { + if (bytesAvailable < 6) return; + var chunkInfo:ChunkInfo = readChunkInfo(dataPosition); + switch (chunkInfo.id) { + // Float RGB + case 0x0010: + var r:int = Math.round(Math.max(0, 255*Math.min(1, data.readFloat()))); + var g:int = Math.round(Math.max(0, 255*Math.min(1, data.readFloat()))); + var b:int = Math.round(Math.max(0, 255*Math.min(1, data.readFloat()))); + object.lightColor = r*65536 + g*256 + b; + break; + // Byte RGB + case 0x0011: + r = data.readUnsignedByte(); + g = data.readUnsignedByte(); + b = data.readUnsignedByte(); + object.lightColor = r*65536 + g*256 + b; + break; + // Spot light + case 0x4610: + var x:Number = data.readFloat(); + var y:Number = data.readFloat(); + var z:Number = data.readFloat(); + object.target = new Vector3D(x, y, z); + object.hotspot = data.readFloat(); + object.falloff = data.readFloat(); + break; + // Light is off + case 0x4620: + object.lightOff = true; + break; + // Attenuation is on + case 0x4625: + object.attenuationOn = true; + break; + // Inner range + case 0x4659: + object.innerRange = data.readFloat(); + break; + // Outer range + case 0x465A: + object.outerRange = data.readFloat(); + break; + // Multiplier + case 0x465B: + object.multiplier = data.readFloat(); + break; + default: + break; + } + parseLightSubChunk(object, chunkInfo.nextChunkPosition, bytesAvailable - chunkInfo.size); + } + + private function parseCameraChunk(object:ObjectData, bytesAvailable:int):void { + if (bytesAvailable < 32) return; + var x:Number = data.readFloat(); + var y:Number = data.readFloat(); + var z:Number = data.readFloat(); + object.position = new Vector3D(x, y, z); + x = data.readFloat(); + y = data.readFloat(); + z = data.readFloat(); + object.target = new Vector3D(x, y, z); + object.bank = data.readFloat(); + object.lens = data.readFloat(); + } + + private function buildContent(texturesBaseURL:String, scale:Number, respectSmoothGroups:Boolean):void { + // Calculation of matrices of texture materials + for (var materialName:String in materialDatas) { + var materialData:MaterialData = materialDatas[materialName]; + materialData.material = new ParserMaterial(); + materialData.material.name = materialName; + var mapData:MapData = materialData.diffuseMap; + if (mapData != null) { + if ((mapData.rotation != 0) || + (mapData.offsetU != 0) || + (mapData.offsetV != 0) || + (mapData.scaleU != 1) || + (mapData.scaleV != 1)) { + // transformation of texture is set + var materialMatrix:Matrix = new Matrix(); + var rot:Number = mapData.rotation*Math.PI/180; + materialMatrix.translate(-mapData.offsetU, mapData.offsetV); + materialMatrix.translate(-0.5, -0.5); + materialMatrix.scale(mapData.scaleU, mapData.scaleV); + materialMatrix.rotate(-rot); + materialMatrix.translate(0.5, 0.5); + materialData.matrix = materialMatrix; + } + } + for each (mapData in materialData.maps) { + materialData.material.textures[mapData.channel] = new ExternalTextureResource(texturesBaseURL + mapData.filename); + } + materialData.material.colors["ambient"] = materialData.ambient; + materialData.material.colors["diffuse"] = materialData.diffuse; + materialData.material.colors["specular"] = materialData.specular; + materialData.material.transparency = 0.01*materialData.transparency; + materials.push(materialData.material); + } + var objectName:String; + var objectData:ObjectData; + var object:Object3D; + // Scene has hierarchically related objects and (or) specified data about objects transformations. + if (animationDatas != null) { + if (objectDatas != null) { + var i:int; + var length:int = animationDatas.length; + var animationData:AnimationData; + for (i = 0; i < length; i++) { + animationData = animationDatas[i]; + objectName = animationData.objectName; + objectData = objectDatas[objectName]; + // Check for instances + if (objectData != null) { + for (var j:int = i + 1; j < length; j++) { + var animationData2:AnimationData = animationDatas[j]; + if (objectName == animationData2.instanceOf) { + animationData2.instanceOf = null; + // Found match name for current part of animation, so make reference for it. + var newObjectData:ObjectData = new ObjectData(); + newObjectData.name = animationData2.objectName; + objectDatas[animationData2.objectName] = newObjectData; + + newObjectData.vertices = objectData.vertices; + newObjectData.uvs = objectData.uvs; + newObjectData.faces = objectData.faces; + newObjectData.surfaces = objectData.surfaces; + newObjectData.surfacesCount = objectData.surfacesCount; + newObjectData.smoothGroups = objectData.smoothGroups; + newObjectData.a = objectData.a; + newObjectData.b = objectData.b; + newObjectData.c = objectData.c; + newObjectData.d = objectData.d; + newObjectData.e = objectData.e; + newObjectData.f = objectData.f; + newObjectData.g = objectData.g; + newObjectData.h = objectData.h; + newObjectData.i = objectData.i; + newObjectData.j = objectData.j; + newObjectData.k = objectData.k; + newObjectData.l = objectData.l; + newObjectData.lightColor = objectData.lightColor; + newObjectData.lightOff = objectData.lightOff; + newObjectData.attenuationOn = objectData.attenuationOn; + newObjectData.hotspot = objectData.hotspot; + newObjectData.falloff = objectData.falloff; + newObjectData.innerRange = objectData.innerRange; + newObjectData.outerRange = objectData.outerRange; + newObjectData.multiplier = objectData.multiplier; + newObjectData.position = objectData.position; + newObjectData.target = objectData.target; + newObjectData.bank = objectData.bank; + newObjectData.lens = objectData.lens; + } + } + } + + if (objectData != null) { + object = buildObject3D(objectData, animationData, scale, respectSmoothGroups); + } else { + // Create empty Object3D + object = new Object3D(); + } + object.name = objectName; + animationData.object = object; + if (animationData.position != null) { + object.x = animationData.position.x*scale; + object.y = animationData.position.y*scale; + object.z = animationData.position.z*scale; + } + if (animationData.rotation != null) { + object.rotationX = animationData.rotation.x; + object.rotationY = animationData.rotation.y; + object.rotationZ = animationData.rotation.z; + } + if (animationData.scale != null) { + object.scaleX = animationData.scale.x; + object.scaleY = animationData.scale.y; + object.scaleZ = animationData.scale.z; + } + } + // Add objects + for (i = 0; i < length; i++) { + animationData = animationDatas[i]; + objects.push(animationData.object); + if (animationData.parentIndex == 0xFFFF) { + hierarchy.push(animationData.object); + } else { + AnimationData(animationDatas[animationData.parentIndex]).object.addChild(animationData.object); + } + } + } + // Scene has no hierarchically related objects and data about objects transformations is not specified. Only polygonal objects woll added to container. + } else { + for (objectName in objectDatas) { + objectData = objectDatas[objectName]; + if (objectData.vertices != null) { + object = buildObject3D(objectData, null, scale, respectSmoothGroups); + object.name = objectName; + objects.push(object); + hierarchy.push(object); + } + } + } + } + + private function buildObject3D(objectData:ObjectData, animationData:AnimationData, scale:Number, respectSmoothGroups:Boolean):Object3D { + var object:Object3D; + if (objectData.vertices != null) { + // Create polygonal object + object = new Mesh(); + buildMesh(object as Mesh, objectData, animationData, scale, respectSmoothGroups); + } else { + if (objectData.lightColor >= 0) { + // Light + var innerRange:Number = 0; + var outerRange:Number = 1e15; // must be Number.MAX_VALUE, but if you set radius to ~2^60 and more then SpotLight is not working. + if (objectData.attenuationOn && (objectData.outerRange < Number.MAX_VALUE)) { + innerRange = objectData.innerRange*scale; + outerRange = objectData.outerRange*scale; + } + if (objectData.target != null) { + var rad:Number = Math.PI/180; + object = new SpotLight(objectData.lightColor, innerRange, outerRange, objectData.hotspot*rad, objectData.falloff*rad); + } else { + object = new OmniLight(objectData.lightColor, innerRange, outerRange); + } + // Light intensity + Light3D(object).intensity = objectData.lightOff ? 0 : objectData.multiplier; + } else { + // Camera or something else + object = new Object3D; + } + if (objectData.position) { + object.x = objectData.position.x*scale; + object.y = objectData.position.y*scale; + object.z = objectData.position.z*scale; + if (objectData.target) { + // Turn object to target + var dx:Number = objectData.target.x*scale - object.x; + var dy:Number = objectData.target.y*scale - object.y; + var dz:Number = objectData.target.z*scale - object.z; + object.rotationX = (Math.atan2(dz, Math.sqrt(((dx*dx) + (dy*dy)))) - (Math.PI/2)); + object.rotationY = 0; + object.rotationZ = -(Math.atan2(dx, dy)); + // Pitch + var matrix:Matrix3D = object.matrix; + matrix.prependRotation(objectData.bank, Vector3D.Z_AXIS); + object.matrix = matrix; + } + } + } + return object; + } + + private function buildMesh(mesh:Mesh, objectData:ObjectData, animationData:AnimationData, scale:Number, respectSmoothGroups:Boolean):void { + // Quit early + if (objectData.faces == null) { + return; + } + + var vertices:Vector. = new Vector.(objectData.vertices.length/3); + var faces:Array = new Array(objectData.faces.length/3); // Vector. can't .sortOn() + + buildInitialGeometry(vertices, faces, objectData, animationData, scale); + + if (respectSmoothGroups) { + cloneVerticesToRespectSmoothGroups(vertices, faces); + } + + calculateVertexNormals(vertices, faces); + + if (materialDatas != null) { + assignMaterialsToFaces(faces, objectData); + + cloneAndTransformVerticesToRespectUVTransforms(vertices, faces); + } + + calculateVertexTangents(vertices, faces); + + // Default material for the faces without surfaces. + var defaultMaterialData:MaterialData = new MaterialData; + defaultMaterialData.numTriangles = 0; + defaultMaterialData.material = new ParserMaterial; + defaultMaterialData.material.colors["diffuse"] = 0x7F7F7F; + defaultMaterialData.material.name = "default"; + + var indices:Vector. = collectFacesIntoSurfaces(faces, defaultMaterialData); + + // Put all to mesh + var vec:Vector3D, vertex:Vertex; + var numVertices:int = vertices.length; + var byteArray:ByteArray = new ByteArray(); + byteArray.endian = Endian.LITTLE_ENDIAN; + for (var n:int = 0; n < numVertices; n++) { + vertex = vertices [n]; + byteArray.writeFloat(vertex.x); + byteArray.writeFloat(vertex.y); + byteArray.writeFloat(vertex.z); + byteArray.writeFloat(vertex.u); + byteArray.writeFloat(vertex.v); + + vec = vertex.normal; + byteArray.writeFloat(vec.x); + byteArray.writeFloat(vec.y); + byteArray.writeFloat(vec.z); + + vec = vertex.tangent; + byteArray.writeFloat(vec.x); + byteArray.writeFloat(vec.y); + byteArray.writeFloat(vec.z); + byteArray.writeFloat(vec.w); + } + mesh.geometry = new Geometry; + mesh.geometry._indices = indices; + mesh.geometry.addVertexStream([ + VertexAttributes.POSITION, + VertexAttributes.POSITION, + VertexAttributes.POSITION, + VertexAttributes.TEXCOORDS[0], + VertexAttributes.TEXCOORDS[0], + VertexAttributes.NORMAL, + VertexAttributes.NORMAL, + VertexAttributes.NORMAL, + VertexAttributes.TANGENT4, + VertexAttributes.TANGENT4, + VertexAttributes.TANGENT4, + VertexAttributes.TANGENT4 + ]); + mesh.geometry._vertexStreams[0].data = byteArray; + mesh.geometry._numVertices = numVertices; + if (objectData.surfaces != null) { + for (var key:String in objectData.surfaces) { + var materialData:MaterialData = materialDatas[key]; + mesh.addSurface(materialData.material, 3*materialData.indexBegin, materialData.numTriangles); + } + } + if (defaultMaterialData.numTriangles > 0) { + mesh.addSurface(defaultMaterialData.material, 3*defaultMaterialData.indexBegin, defaultMaterialData.numTriangles); + } + mesh.calculateBoundBox(); + } + + private function buildInitialGeometry(vertices:Vector., faces:Array, objectData:ObjectData, animationData:AnimationData, scale:Number):void { + var correct:Boolean = false; + if (animationData != null) { + var a:Number = objectData.a; + var b:Number = objectData.b; + var c:Number = objectData.c; + var d:Number = objectData.d; + var e:Number = objectData.e; + var f:Number = objectData.f; + var g:Number = objectData.g; + var h:Number = objectData.h; + var i:Number = objectData.i; + var j:Number = objectData.j; + var k:Number = objectData.k; + var l:Number = objectData.l; + var det:Number = 1/(-c*f*i + b*g*i + c*e*j - a*g*j - b*e*k + a*f*k); + objectData.a = (-g*j + f*k)*det; + objectData.b = (c*j - b*k)*det; + objectData.c = (-c*f + b*g)*det; + objectData.d = (d*g*j - c*h*j - d*f*k + b*h*k + c*f*l - b*g*l)*det; + objectData.e = (g*i - e*k)*det; + objectData.f = (-c*i + a*k)*det; + objectData.g = (c*e - a*g)*det; + objectData.h = (c*h*i - d*g*i + d*e*k - a*h*k - c*e*l + a*g*l)*det; + objectData.i = (-f*i + e*j)*det; + objectData.j = (b*i - a*j)*det; + objectData.k = (-b*e + a*f)*det; + objectData.l = (d*f*i - b*h*i - d*e*j + a*h*j + b*e*l - a*f*l)*det; + if (animationData.pivot != null) { + objectData.d -= animationData.pivot.x; + objectData.h -= animationData.pivot.y; + objectData.l -= animationData.pivot.z; + } + correct = true; + } + // Creation and correcting of vertices + var n:int, m:int, p:int, len:int = objectData.vertices.length; + var uv:Boolean = objectData.uvs != null && objectData.uvs.length > 0; + for (n = 0, m = 0, p = 0; n < len;) { + var vertex:Vertex = new Vertex; + if (correct) { + var x:Number = objectData.vertices[n++]; + var y:Number = objectData.vertices[n++]; + var z:Number = objectData.vertices[n++]; + vertex.x = objectData.a*x + objectData.b*y + objectData.c*z + objectData.d; + vertex.y = objectData.e*x + objectData.f*y + objectData.g*z + objectData.h; + vertex.z = objectData.i*x + objectData.j*y + objectData.k*z + objectData.l; + } else { + vertex.x = objectData.vertices[n++]; + vertex.y = objectData.vertices[n++]; + vertex.z = objectData.vertices[n++]; + } + vertex.x *= scale; + vertex.y *= scale; + vertex.z *= scale; + if (uv) { + vertex.u = objectData.uvs[m++]; + vertex.v = 1 - objectData.uvs[m++]; + } else { + // If you leave object without uv, then the calculation of tangents is breaks + x = vertex.x; + y = vertex.y; + var rxy:Number = 1e-5 + Math.sqrt(x*x + y*y); + vertex.u = Math.atan2(rxy, vertex.z); + vertex.v = Math.atan2(y, x); + } + vertices[p++] = vertex; + } + // Create faces + len = objectData.faces.length; + for (n = 0, p = 0; n < len;) { + var face:Face = new Face(); + face.a = objectData.faces[n++]; + face.b = objectData.faces[n++]; + face.c = objectData.faces[n++]; + face.smoothGroup = objectData.smoothGroups[p]; + faces[p++] = face; + } + } + + private function cloneVerticesToRespectSmoothGroups(vertices:Vector., faces:Array):void { + // Actions with smoothing groups: + // - if vertex is in faces with groups 1+2 and 3, then it is duplicated + // - if vertex is in faces with groups 1+2, 3 and 1+3, then it is not duplicated + + var n:int, m:int, p:int, q:int, len:int, numVertices:int = vertices.length, numFaces:int = faces.length; + // Calculate disjoint groups for vertices + var vertexGroups:Vector.> = new Vector.>(numVertices, true); + for (p = 0; p < numVertices; p++) { + vertexGroups [p] = new Vector.; + } + for (n = 0; n < numFaces; n++) { + var face:Face = Face(faces[n]); + for (m = 0; m < 3; m++) { + var groups:Vector. = vertexGroups [(m == 0) ? face.a : ((m == 1) ? face.b : face.c)]; + var group:uint = face.smoothGroup; + for (q = groups.length - 1; q >= 0; q--) { + if ((group & groups [q]) > 0) { + group |= groups [q]; + groups.splice(q, 1); + q = groups.length - 1; + } + } + groups.push(group); + } + } + // Clone vertices + var vertexClones:Vector.> = new Vector.>(numVertices, true); + for (p = 0; p < numVertices; p++) { + if ((len = vertexGroups [p].length) < 1) continue; + var clones:Vector. = new Vector.(len, true); + vertexClones [p] = clones; + clones [0] = p; + var vertex0:Vertex = vertices [p]; + for (m = 1; m < len; m++) { + var vertex1:Vertex = new Vertex; + vertex1.x = vertex0.x; + vertex1.y = vertex0.y; + vertex1.z = vertex0.z; + vertex1.u = vertex0.u; + vertex1.v = vertex0.v; + clones[m] = vertices.length; + vertices.push(vertex1); + } + } + numVertices = vertices.length; + + // Loop on faces + for (n = 0; n < numFaces; n++) { + face = Face(faces [n]); + group = face.smoothGroup; + + for (m = 0; m < 3; m++) { + p = (m == 0) ? face.a : ((m == 1) ? face.b : face.c); + groups = vertexGroups [p]; + len = groups.length; + clones = vertexClones [p]; + for (q = 0; q < len; q++) { + if (((group == 0) && (groups [q] == 0)) || + ((group & groups [q]) > 0)) { + var index:uint = clones [q]; + if (group == 0) { + // In case of there is no smoothing group, vertices of this face is unique + groups.splice(q, 1); + clones.splice(q, 1); + } + if (m == 0) face.a = index; else + if (m == 1) face.b = index; else + face.c = index; + q = len; + } + } + } + } + } + + private function cloneAndTransformVerticesToRespectUVTransforms(vertices:Vector., faces:Array):void { + // Actions with UV transformation + // if vertex in faces with different transform materials, then it is duplicated + var n:int, m:int, p:int, q:int, len:int, numVertices:int = vertices.length, numFaces:int = faces.length; + // Find transform materials for vertices + var vertexGroups:Vector.> = new Vector.>(numVertices, true); + for (p = 0; p < numVertices; p++) { + vertexGroups [p] = new Vector.; + } + for (n = 0; n < numFaces; n++) { + var face:Face = Face(faces [n]); + for (m = 0; m < 3; m++) { + var groups:Vector. = vertexGroups [(m == 0) ? face.a : ((m == 1) ? face.b : face.c)]; + var group:uint = face.uvTransformGroup; + if (groups.indexOf(group) < 0) groups.push(group); + } + } + // Clone vertices + var vertexClones:Vector.> = new Vector.>(numVertices, true); + for (p = 0; p < numVertices; p++) { + if ((len = vertexGroups [p].length) < 1) continue; + var clones:Vector. = new Vector.(len, true); + vertexClones [p] = clones; + clones [0] = p; + var vertex0:Vertex = vertices [p]; + for (m = 1; m < len; m++) { + var vertex1:Vertex = new Vertex; + vertex1.x = vertex0.x; + vertex1.y = vertex0.y; + vertex1.z = vertex0.z; + vertex1.u = vertex0.u; + vertex1.v = vertex0.v; + vertex1.normal = vertex0.normal; + clones [m] = vertices.length; + vertices.push(vertex1); + } + } + numVertices = vertices.length; + // Parse on faces, and apply the transformation + for (n = 0; n < numFaces; n++) { + face = Face(faces [n]); + group = face.uvTransformGroup; + + var materialData:MaterialData = materialDatas[face.surfaceName]; + + for (m = 0; m < 3; m++) { + p = (m == 0) ? face.a : ((m == 1) ? face.b : face.c); + groups = vertexGroups [p]; + len = groups.length; + clones = vertexClones [p]; + q = groups.indexOf(group); // must aways be in groups + var index:uint = clones [q]; + if (m == 0) face.a = index; else + if (m == 1) face.b = index; else + face.c = index; + + if (group > 0) { + vertex0 = vertices [index]; + if (vertex0.nonTransformed) { + vertex0.nonTransformed = false; + var u:Number = vertex0.u; + var v:Number = vertex0.v; + vertex0.u = materialData.matrix.a*u + materialData.matrix.b*v + materialData.matrix.tx; + vertex0.v = materialData.matrix.c*u + materialData.matrix.d*v + materialData.matrix.ty; + } + } + } + } + } + + private function calculateVertexNormals(vertices:Vector., faces:Array):void { + var n:int, m:int, numFaces:int = faces.length; + for (n = 0; n < numFaces; n++) { + var face:Face = Face(faces [n]); + + // Calculation of average normals of vertices + var vertex0:Vertex = vertices [face.a]; + var vertex1:Vertex = vertices [face.b]; + var vertex2:Vertex = vertices [face.c]; + + var deltaX1:Number = vertex1.x - vertex0.x; + var deltaY1:Number = vertex1.y - vertex0.y; + var deltaZ1:Number = vertex1.z - vertex0.z; + var deltaX2:Number = vertex2.x - vertex0.x; + var deltaY2:Number = vertex2.y - vertex0.y; + var deltaZ2:Number = vertex2.z - vertex0.z; + + face.deltaX1 = deltaX1; + face.deltaY1 = deltaY1; + face.deltaZ1 = deltaZ1; + face.deltaX2 = deltaX2; + face.deltaY2 = deltaY2; + face.deltaZ2 = deltaZ2; + + var normalX:Number = deltaZ2*deltaY1 - deltaY2*deltaZ1; + var normalY:Number = deltaX2*deltaZ1 - deltaZ2*deltaX1; + var normalZ:Number = deltaY2*deltaX1 - deltaX2*deltaY1; + + var normalLen:Number = 1e-5 + Math.sqrt(normalX*normalX + normalY*normalY + normalZ*normalZ); + normalX = normalX/normalLen; + normalY = normalY/normalLen; + normalZ = normalZ/normalLen; + + for (m = 0; m < 3; m++) { + var vertex:Vertex = (m == 0) ? vertex0 : ((m == 1) ? vertex1 : vertex2); + if (vertex.normal == null) { + vertex.normal = new Vector3D(normalX, normalY, normalZ); + } else { + var vec:Vector3D = vertex.normal; + vec.x += normalX; + vec.y += normalY; + vec.z += normalZ; + } + } + } + } + + private function calculateVertexTangents(vertices:Vector., faces:Array):void { + var n:int, m:int, numVertices:int = vertices.length, numFaces:int = faces.length; + for (n = 0; n < numFaces; n++) { + var face:Face = Face(faces [n]); + + // Calculation of average tangents of vertices + var vertex0:Vertex = vertices [face.a]; + var vertex1:Vertex = vertices [face.b]; + var vertex2:Vertex = vertices [face.c]; + + var deltaU1:Number = vertex1.u - vertex0.u; + var deltaV1:Number = vertex0.v - vertex1.v; + var deltaU2:Number = vertex2.u - vertex0.u; + var deltaV2:Number = vertex0.v - vertex2.v; + + // Inverse determinant is included to the formulas below as common multiplier. + // Its value is insignificantly, because vectors are normalized + //var invdet:Number = 1 / (deltaU1 * deltaV2 - deltaU2 * deltaV1); + //if (invdet > 1e9) invdet = 1e9; else if (invdet < -1e9) invdet = -1e9; + + var deltaX1:Number = face.deltaX1; + var deltaY1:Number = face.deltaY1; + var deltaZ1:Number = face.deltaZ1; + var deltaX2:Number = face.deltaX2; + var deltaY2:Number = face.deltaY2; + var deltaZ2:Number = face.deltaZ2; + + var stMatrix00:Number = (deltaV2)//*invdet; + var stMatrix01:Number = -(deltaV1)//*invdet; + var stMatrix10:Number = -(deltaU2)//*invdet; + var stMatrix11:Number = (deltaU1)//*invdet; + + var tangentX:Number = stMatrix00*deltaX1 + stMatrix01*deltaX2; + var tangentY:Number = stMatrix00*deltaY1 + stMatrix01*deltaY2; + var tangentZ:Number = stMatrix00*deltaZ1 + stMatrix01*deltaZ2; + + var biTangentX:Number = stMatrix10*deltaX1 + stMatrix11*deltaX2; + var biTangentY:Number = stMatrix10*deltaY1 + stMatrix11*deltaY2; + var biTangentZ:Number = stMatrix10*deltaZ1 + stMatrix11*deltaZ2; + + var tangentLen:Number = 1e-5 + Math.sqrt(tangentX*tangentX + tangentY*tangentY + tangentZ*tangentZ); + tangentX = tangentX/tangentLen; + tangentY = tangentY/tangentLen; + tangentZ = tangentZ/tangentLen; + + var biTangentLen:Number = 1e-5 + Math.sqrt(biTangentX*biTangentX + biTangentY*biTangentY + biTangentZ*biTangentZ); + biTangentX = biTangentX/biTangentLen; + biTangentY = biTangentY/biTangentLen; + biTangentZ = biTangentZ/biTangentLen; + + for (m = 0; m < 3; m++) { + var vertex:Vertex = (m == 0) ? vertex0 : ((m == 1) ? vertex1 : vertex2); + if (vertex.tangent == null) { + vertex.tangent = new Vector3D(tangentX, tangentY, tangentZ); + vertex.biTangent = new Vector3D(biTangentX, biTangentY, biTangentZ); + } else { + var vec:Vector3D; + vec = vertex.tangent; + vec.x += tangentX; + vec.y += tangentY; + vec.z += tangentZ; + vec = vertex.biTangent; + vec.x += biTangentX; + vec.y += biTangentY; + vec.z += biTangentZ; + } + } + } + + // orthonormalize TBN's + var normalX:Number, normalY:Number, normalZ:Number, dot:Number, vec2:Vector3D; + for (n = 0; n < numVertices; n++) { + vertex = vertices [n]; + if (vertex.normal == null) { + vertex.normal = Vector3D.X_AXIS; + vertex.tangent = Vector3D.Y_AXIS.clone(); + vertex.tangent.w = 1; + } else { + vec = vertex.normal; + vec.normalize(); + normalX = vec.x; + normalY = vec.y; + normalZ = vec.z; + + vec = vertex.tangent; + tangentX = vec.x; + tangentY = vec.y; + tangentZ = vec.z; + + dot = normalX*tangentX + normalY*tangentY + normalZ*tangentZ; + + // perform orthonormalization between normal and tangent: tangent -= normal*dot; + tangentX -= normalX*dot; + tangentY -= normalY*dot; + tangentZ -= normalZ*dot; + + tangentLen = tangentX*tangentX + tangentY*tangentY + tangentZ*tangentZ; + if (tangentLen > 0) { + tangentLen = Math.sqrt(tangentLen); + vec.x = tangentX/tangentLen; + vec.y = tangentY/tangentLen; + vec.z = tangentZ/tangentLen; + + // calculate direction of bi-normal + var crossX:Number = normalY*tangentZ - normalZ*tangentY; + var crossY:Number = normalZ*tangentX - normalX*tangentZ; + var crossZ:Number = normalX*tangentY - normalY*tangentX; + + vec2 = vertex.biTangent; + + dot = crossX*vec2.x + crossY*vec2.y + crossZ*vec2.z; + vec.w = (dot < 0) ? -1 : 1; + } else { + // tangent is degenerate, try to start from bi-normal + vec = vertex.biTangent; + biTangentX = vec.x; + biTangentY = vec.y; + biTangentZ = vec.z; + + dot = normalX*biTangentX + normalY*biTangentY + normalZ*biTangentZ; + + // perform orthonormalization between normal and bi-normal: biTangent -= normal*dot; + biTangentX -= normalX*dot; + biTangentY -= normalY*dot; + biTangentZ -= normalZ*dot; + + biTangentLen = biTangentX*biTangentX + biTangentY*biTangentY + biTangentZ*biTangentZ; + if (biTangentLen > 0) { + biTangentLen = Math.sqrt(biTangentLen); + vec.x = biTangentX/biTangentLen; + vec.y = biTangentY/biTangentLen; + vec.z = biTangentZ/biTangentLen; + } else { + // bi-normal is degenerate too, get any vector that is perpendicular to the normal + if (normalX != 0) { + vec.x = -normalY; + vec.y = normalX; + vec.z = 0; + } else { + vec.x = 0; + vec.y = -normalZ; + vec.z = normalY; + } + } + + // calculate tangent + biTangentX = vec.x; + biTangentY = vec.y; + biTangentZ = vec.z; + + vec = vertex.tangent; + vec.x = -(normalY*biTangentZ - normalZ*biTangentY); + vec.y = -(normalZ*biTangentX - normalX*biTangentZ); + vec.z = -(normalX*biTangentY - normalY*biTangentX); + + dot = biTangentX*vec.x + biTangentY*vec.y + biTangentZ*vec.z; + vec.w = (dot < 0) ? -1 : 1; + } + } + } + } + + private function assignMaterialsToFaces(faces:Array, objectData:ObjectData):void { + // Assign materials + if (objectData.surfaces != null) { + for (var key:String in objectData.surfaces) { + var surface:Vector. = objectData.surfaces[key]; + // Get serial number of material for sorting + var surfaceIndex:uint = surface.pop(); + var materialData:MaterialData = materialDatas[key]; + for (var n:int = 0; n < surface.length; n++) { + var face:Face = faces[surface[n]]; + face.surface = surfaceIndex; + face.surfaceName = key; + // If it need to correct UV-coordianates + face.uvTransformGroup = (materialData.matrix != null) ? surfaceIndex : 0; + } + } + } + } + + private function collectFacesIntoSurfaces(faces:Array, defaultMaterialData:MaterialData):Vector. { + // Sort faces on materials + faces.sortOn("surface"); + + // Create indices, calculate indexBegin and numTriangles + var numFaces:int = faces.length; + var indices:Vector. = new Vector.(numFaces*3, true); + + var lastMaterialData:MaterialData; + for (var n:int = 0; n < numFaces; n++) { + var face:Face = Face(faces [n]); + + var m:int = n*3; + indices [m] = face.a; + indices [m + 1] = face.b; + indices [m + 2] = face.c; + + var materialData:MaterialData = defaultMaterialData; + if (face.surfaceName != null) { + materialData = materialDatas[face.surfaceName]; + } + if (lastMaterialData != materialData) { + lastMaterialData = materialData; + materialData.indexBegin = n; + materialData.numTriangles = 1; + } else { + materialData.numTriangles++; + } + } + return indices; + } + } +} + +import alternativa.engine3d.core.Object3D; +import alternativa.engine3d.loaders.ParserMaterial; + +import flash.geom.Matrix; +import flash.geom.Vector3D; + +class MaterialData { + public var name:String; + public var ambient:uint; + public var diffuse:uint; + public var specular:uint; + public var glossiness:uint; + public var transparency:uint; + public var matrix:Matrix; + public var material:ParserMaterial; + + public var maps:Vector. = new Vector.(); + + public function get diffuseMap():MapData { + for each (var map:MapData in maps) { + if (map.channel == "diffuse") { + return map; + } + } + return null; + } + + // parameters for Mesh.addSurface() + public var indexBegin:uint; + public var numTriangles:uint; +} + +class MapData { + public var channel:String; + public var filename:String; + public var scaleU:Number = 1; + public var scaleV:Number = 1; + public var offsetU:Number = 0; + public var offsetV:Number = 0; + public var rotation:Number = 0; +} + +class ObjectData { + public var name:String; + // mesh + public var vertices:Vector.; + public var uvs:Vector.; + public var faces:Vector.; + public var surfaces:Object; + public var surfacesCount:uint; + public var smoothGroups:Vector.; + public var a:Number; + public var b:Number; + public var c:Number; + public var d:Number; + public var e:Number; + public var f:Number; + public var g:Number; + public var h:Number; + public var i:Number; + public var j:Number; + public var k:Number; + public var l:Number; + // light or camera + public var lightColor:int = -1; + public var lightOff:Boolean; + public var attenuationOn:Boolean; + public var hotspot:Number = 0; + public var falloff:Number = 0; + public var innerRange:Number = 0; + public var outerRange:Number = Number.MAX_VALUE; + public var multiplier:Number = 1; + public var position:Vector3D; + public var target:Vector3D; + public var bank:Number = 0; + public var lens:Number; +} + +class AnimationData { + public var chunkId:uint; + public var objectName:String; + public var object:Object3D; + public var parentIndex:int; + public var pivot:Vector3D; + public var position:Vector3D; + public var rotation:Vector3D; + public var scale:Vector3D; + public var instanceOf:String; +} + +class ChunkInfo { + public var id:int; + public var size:int; + public var dataSize:int; + public var dataPosition:int; + public var nextChunkPosition:int; +} + +class Vertex { + public var x:Number; + public var y:Number; + public var z:Number; + public var u:Number; + public var v:Number; + public var nonTransformed:Boolean = true; + public var normal:Vector3D; + public var tangent:Vector3D; + public var biTangent:Vector3D; +} + +class Face { + public var a:uint; + public var b:uint; + public var c:uint; + public var surface:uint; + public var surfaceName:String; + public var smoothGroup:uint; + public var uvTransformGroup:uint; + public var deltaX1:Number; + public var deltaY1:Number; + public var deltaZ1:Number; + public var deltaX2:Number; + public var deltaY2:Number; + public var deltaZ2:Number; +} diff --git a/src/alternativa/engine3d/loaders/ParserA3D.as b/src/alternativa/engine3d/loaders/ParserA3D.as new file mode 100644 index 0000000..a1b94da --- /dev/null +++ b/src/alternativa/engine3d/loaders/ParserA3D.as @@ -0,0 +1,188 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders { + + import alternativa.engine3d.alternativa3d; + import alternativa.osgi.OSGi; + import alternativa.osgi.service.clientlog.IClientLog; + import alternativa.protocol.ICodec; + import alternativa.protocol.IProtocol; + import alternativa.protocol.OptionalMap; + import alternativa.protocol.ProtocolBuffer; + import alternativa.protocol.impl.OptionalMapCodecHelper; + import alternativa.protocol.impl.PacketHelper; + import alternativa.protocol.impl.Protocol; + import alternativa.protocol.info.TypeCodecInfo; + import alternativa.protocol.osgi.ProtocolActivator; + + import flash.utils.ByteArray; + + import platform.client.formats.a3d.osgi.Activator; + import platform.clients.fp10.libraries.alternativaprotocol.Activator; + + import versions.version1.a3d.A3D; + import versions.version2.a3d.A3D2; + import versions.version2.a3d.A3D2Extra1; + import versions.version2.a3d.A3D2Extra2; + + use namespace alternativa3d; + +/** + * A parser for loading models of A3D binary format. + * A3D format reference you can find here. + */ +public class ParserA3D extends Parser { + +// static public const logChannel:String = "ParserLog"; + + private var protocol:Protocol; + + private var wasInit:Boolean = false; + + /** + * Creates a new instance of ParserA3D. + * + */ + public function ParserA3D() { + init(); + } + + /** + * Parses model of a3d format, that is passed as byteArray to input parameter, then fills the arrays objects and hierarchy by the instances of three-dimensional objects. + * @param input ByteArray consists of A3D data. + */ + public function parse(input:ByteArray):void { + try { + input.position = 0; + var version:int = input.readByte(); + if (version == 0) { + // For the 1st version of format + parseVersion1(input); + } else { + // For the 2nd version of format and above, the first byte contains length of file and flag bits. + // Bit of packing. It always equal to 1, because version 2 and above is always packed. + parseVersionOver1(input); + } + } catch (e:Error) { + e.message = "Parsing failed: " + e.message; + throw e; + } + + } + + private function init():void { + if (wasInit) return; + var osgi:OSGi; + if (OSGi.getInstance() == null) { + osgi = new OSGi(); + OSGi.clientLog = new DummyClientLog(); + osgi.registerService(IClientLog, new DummyClientLog()); + new ProtocolActivator().start(osgi); + new platform.clients.fp10.libraries.alternativaprotocol.Activator().start(osgi); + } else { + osgi = OSGi.getInstance(); + } + new platform.client.formats.a3d.osgi.Activator().start(osgi); + protocol = Protocol(osgi.getService(IProtocol)); + wasInit = true; + } + + private function parseVersion1(input:ByteArray):void { + input.position = 4; + var nullMap:OptionalMap = OptionalMapCodecHelper.decodeNullMap(input); + nullMap.setReadPosition(0); + var data:ByteArray = new ByteArray(); + data.writeBytes(input, input.position); + data.position = 0; + var buffer:ProtocolBuffer = new ProtocolBuffer(data, data, nullMap); + var codec:ICodec = protocol.getCodec(new TypeCodecInfo(A3D, false)); + var _a3d:A3D = A3D(codec.decode(buffer)); + complete(_a3d); + } + + private function parseVersionOver1(input:ByteArray):void { + input.position = 0; + var data:ByteArray = new ByteArray(); + var buffer:ProtocolBuffer = new ProtocolBuffer(data, data, new OptionalMap()); + PacketHelper.unwrapPacket(input, buffer); + input.position = 0; + var versionMajor:int = buffer.reader.readUnsignedShort(); + var versionMinor:int = buffer.reader.readUnsignedShort(); + switch (versionMajor) { + case 2: + if (versionMinor >= 6) { + compressedBuffers = true; + } + var parts:Vector. = new Vector.(); + parts.push(parseVersion2_0(buffer)); + if (versionMinor >= 4) { + parts.push(parseVersion2_4(buffer)); + } + if (versionMinor >= 5) { + parts.push(parseVersion2_5(buffer)); + } + complete(parts); + break; + } + } + + private function parseVersion2_0(buffer:ProtocolBuffer):Object { + var codec:ICodec = protocol.getCodec(new TypeCodecInfo(A3D2, false)); + var a3d:A3D2 = A3D2(codec.decode(buffer)); + return a3d; + } + + private function parseVersion2_5(buffer:ProtocolBuffer):Object { + var codec:ICodec = protocol.getCodec(new TypeCodecInfo(A3D2Extra2, false)); + var a3d:A3D2Extra2 = A3D2Extra2(codec.decode(buffer)); + return a3d; + } + + private function parseVersion2_4(buffer:ProtocolBuffer):Object { + var codec:ICodec = protocol.getCodec(new TypeCodecInfo(A3D2Extra1, false)); + var a3d:A3D2Extra1 = A3D2Extra1(codec.decode(buffer)); + return a3d; + } + +} +} + +import alternativa.osgi.service.clientlog.IClientLog; +import alternativa.osgi.service.clientlog.IClientLogChannelListener; + +class DummyClientLog implements IClientLog { + + public function logError(channelName:String, text:String, ...vars):void { + } + + public function log(channelName:String, text:String, ...rest):void { + } + + public function getChannelStrings(channelName:String):Vector. { + return null; + } + + public function addLogListener(listener:IClientLogChannelListener):void { + } + + public function removeLogListener(listener:IClientLogChannelListener):void { + } + + public function addLogChannelListener(channelName:String, listener:IClientLogChannelListener):void { + } + + public function removeLogChannelListener(channelName:String, listener:IClientLogChannelListener):void { + } + + public function getChannelNames():Vector. { + return null; + } +} diff --git a/src/alternativa/engine3d/loaders/ParserCollada.as b/src/alternativa/engine3d/loaders/ParserCollada.as new file mode 100644 index 0000000..39a07be --- /dev/null +++ b/src/alternativa/engine3d/loaders/ParserCollada.as @@ -0,0 +1,391 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.animation.AnimationClip; + import alternativa.engine3d.animation.keys.Track; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.loaders.collada.DaeDocument; + import alternativa.engine3d.loaders.collada.DaeMaterial; + import alternativa.engine3d.loaders.collada.DaeNode; + import alternativa.engine3d.loaders.collada.DaeObject; + import alternativa.engine3d.resources.ExternalTextureResource; + + use namespace alternativa3d; + + /** + * A parser performs parsing of collada xml. + */ + public class ParserCollada extends Parser { + + /** + * List of the light sources + */ + public var lights:Vector.; + + /** + * Creates a new ParserCollada instance. + */ + public function ParserCollada() { + } + + /** + * Clears all links to external objects. + */ + override public function clean():void { + super.clean(); + lights = null; + } + + /** + * @private + * Initialization before the parsing + */ + override alternativa3d function init():void { + super.init(); + // cameras = new Vector.(); + lights = new Vector.(); + } + + /** + * Method parses xml of collada and fills arrays objects, hierarchy, materials, animations + * If you need to download textures, use class TexturesLoader. + *

Path to collada file should match with URI specification. E.g., file:///C:/test.dae or /C:/test.dae for the full paths and test.dae, ./test.dae in case of relative.

+ * + * @param data XML data type of collada. + * @param baseURL Path to textures relative to swf-file (Or file name only in case of trimPaths=true). + * @param trimPaths Use file names only, without paths. + * + * @see alternativa.engine3d.loaders.TexturesLoader + * @see #objects + * @see #hierarchy + * @see #materials + */ + public function parse(data:XML, baseURL:String = null, trimPaths:Boolean = false):void { + init(); + + var document:DaeDocument = new DaeDocument(data, 0); + if (document.scene != null) { + parseNodes(document.scene.nodes, null, false); + parseMaterials(document.materials, baseURL, trimPaths); + } + } + + /** + * Adds components of animated object to lists objects, parents, hierarchy, cameras, animations and to parent container. + */ + private function addObject(animatedObject:DaeObject, parent:Object3D, layer:String):Object3D { + var object:Object3D = Object3D(animatedObject.object); + this.objects.push(object); + if (parent == null) { + this.hierarchy.push(object); + } else { + parent.addChild(object); + } +// if (object is Camera3D) { +// this.cameras.push(object as Camera3D); +// } + if (object is Light3D) { + lights.push(Light3D(object)); + } + if (animatedObject.animation != null) { + this.animations.push(animatedObject.animation); + } + if (layer) { + layersMap[object] = layer; + } + return object; + } + + /** + * Adds objects to objects, parents, hierarchy, cameras and animations lists and to parent container. + * + * @return first object + */ + private function addObjects(animatedObjects:Vector., parent:Object3D, layer:String):Object3D { + var first:Object3D = addObject(animatedObjects[0], parent, layer); + for (var i:int = 1, count:int = animatedObjects.length; i < count; i++) { + addObject(animatedObjects[i], parent, layer); + } + return first; + } + + /** + * Check if is there a skin among child objects. + */ + private function hasSkinsInChildren(node:DaeNode):Boolean { + var nodes:Vector. = node.nodes; + for (var i:int = 0, count:int = nodes.length; i < count; i++) { + var child:DaeNode = nodes[i]; + child.parse(); + if (child.skins != null) { + return true; + } + if (hasSkinsInChildren(child)) { + return true; + } + } + return false; + } + + private function parseNodes(nodes:Vector., parent:Object3D, skinsOnly:Boolean = false):void { + for (var i:int = 0, count:int = nodes.length; i < count; i++) { + var node:DaeNode = nodes[i]; + node.parse(); + + // Object to which child objects will be added. + var container:Object3D = null; + if (node.skins != null) { + // Main joint of skin + container = addObjects(node.skins, parent, node.layer); + } else { + if (!skinsOnly && !node.skinOrTopmostJoint) { + if (node.objects != null) { + container = addObjects(node.objects, parent, node.layer); + } else { + // Empty Object3D + container = new Object3D(); + container.name = node.cloneString(node.name); + addObject(node.applyAnimation(node.applyTransformations(container)), parent, node.layer); + container.calculateBoundBox(); + } + } else { + // Object or its parent is a skin or joint + // Create an object only if there are a child skins + if (hasSkinsInChildren(node)) { + container = new Object3D(); + container.name = node.cloneString(node.name); + addObject(node.applyAnimation(node.applyTransformations(container)), parent, node.layer); + parseNodes(node.nodes, container, skinsOnly || node.skinOrTopmostJoint); + container.calculateBoundBox(); + } + } + } + // Parse children + if (container != null) { + parseNodes(node.nodes, container, skinsOnly || node.skinOrTopmostJoint); + } + } + } + + private function trimPath(path:String):String { + var index:int = path.lastIndexOf("/"); + return (index < 0) ? path : path.substr(index + 1); + } + + private function parseMaterials(materials:Object, baseURL:String, trimPaths:Boolean):void { + var tmaterial:ParserMaterial; + for each (var material:DaeMaterial in materials) { + if (material.used) { + material.parse(); + this.materials.push(material.material); + } + } + var resource:ExternalTextureResource; + // Prepare paths + if (trimPaths) { + for each (tmaterial in this.materials) { + for each(resource in tmaterial.textures) { + if (resource != null && resource.url != null) { + resource.url = trimPath(fixURL(resource.url)); + } + } + } + } else { + for each (tmaterial in this.materials) { + for each(resource in tmaterial.textures) { + if (resource != null && resource.url != null) { + resource.url = fixURL(resource.url); + } + } + } + } + var base:String; + if (baseURL != null) { + baseURL = fixURL(baseURL); + var end:int = baseURL.lastIndexOf("/"); + base = (end < 0) ? "" : baseURL.substr(0, end); + for each (tmaterial in this.materials) { + for each(resource in tmaterial.textures) { + if (resource != null && resource.url != null) { + resource.url = resolveURL(resource.url, base); + } + } + } + } + } + + /** + * @private + * Prosesses URL with following actions. + * Replaces backslashes with slashes, adds three direct slashes after file: + */ + private function fixURL(url:String):String { + var pathStart:int = url.indexOf("://"); + pathStart = (pathStart < 0) ? 0 : pathStart + 3; + var pathEnd:int = url.indexOf("?", pathStart); + pathEnd = (pathEnd < 0) ? url.indexOf("#", pathStart) : pathEnd; + var path:String = url.substring(pathStart, (pathEnd < 0) ? 0x7FFFFFFF : pathEnd); + path = path.replace(/\\/g, "/"); + var fileIndex:int = url.indexOf("file://"); + if (fileIndex >= 0) { + if (url.charAt(pathStart) == "/") { + return "file://" + path + ((pathEnd >= 0) ? url.substring(pathEnd) : ""); + } + return "file:///" + path + ((pathEnd >= 0) ? url.substring(pathEnd) : ""); + } + return url.substring(0, pathStart) + path + ((pathEnd >= 0) ? url.substring(pathEnd) : ""); + } + + /** + * @private + */ + private function mergePath(path:String, base:String, relative:Boolean = false):String { + var baseParts:Array = base.split("/"); + var parts:Array = path.split("/"); + for (var i:int = 0, count:int = parts.length; i < count; i++) { + var part:String = parts[i]; + if (part == "..") { + var basePart:String = baseParts.pop(); + while (basePart == "." || basePart == "" && basePart != null) basePart = baseParts.pop(); + if (relative) { + if (basePart == "..") { + baseParts.push("..", ".."); + } else if (basePart == null) { + baseParts.push(".."); + } + } + } else { + baseParts.push(part); + } + } + return baseParts.join("/"); + } + + /** + * @private + * Converts relative paths to full paths + */ + private function resolveURL(url:String, base:String):String { + if (base == "") { + return url; + } + // http://labs.apache.org/webarch/uri/rfc/rfc3986.html + if (url.charAt(0) == "." && url.charAt(1) == "/") { + // File at the same folder + return base + url.substr(1); + } else if (url.charAt(0) == "/") { + // Full path + return url; + } else if (url.charAt(0) == "." && url.charAt(1) == ".") { + // Above on level + var queryAndFragmentIndex:int = url.indexOf("?"); + queryAndFragmentIndex = (queryAndFragmentIndex < 0) ? url.indexOf("#") : queryAndFragmentIndex; + var path:String; + var queryAndFragment:String; + if (queryAndFragmentIndex < 0) { + queryAndFragment = ""; + path = url; + } else { + queryAndFragment = url.substring(queryAndFragmentIndex); + path = url.substring(0, queryAndFragmentIndex); + } + // Split base URL on parts + var bPath:String; + var bSlashIndex:int = base.indexOf("/"); + var bShemeIndex:int = base.indexOf(":"); + var bAuthorityIndex:int = base.indexOf("//"); + if (bAuthorityIndex < 0 || bAuthorityIndex > bSlashIndex) { + if (bShemeIndex >= 0 && bShemeIndex < bSlashIndex) { + // Scheme exists, no domain + var bSheme:String = base.substring(0, bShemeIndex + 1); + bPath = base.substring(bShemeIndex + 1); + if (bPath.charAt(0) == "/") { + return bSheme + "/" + mergePath(path, bPath.substring(1), false) + queryAndFragment; + } else { + return bSheme + mergePath(path, bPath, false) + queryAndFragment; + } + } else { + // No Scheme, no domain + if (base.charAt(0) == "/") { + return "/" + mergePath(path, base.substring(1), false) + queryAndFragment; + } else { + return mergePath(path, base, true) + queryAndFragment; + } + } + } else { + bSlashIndex = base.indexOf("/", bAuthorityIndex + 2); + var bAuthority:String; + if (bSlashIndex >= 0) { + bAuthority = base.substring(0, bSlashIndex + 1); + bPath = base.substring(bSlashIndex + 1); + return bAuthority + mergePath(path, bPath, false) + queryAndFragment; + } else { + bAuthority = base; + return bAuthority + "/" + mergePath(path, "", false); + } + } + } + var shemeIndex:int = url.indexOf(":"); + var slashIndex:int = url.indexOf("/"); + if (shemeIndex >= 0 && (shemeIndex < slashIndex || slashIndex < 0)) { + // Contains the schema + return url; + } + return base + "/" + url; + } + + /** + * Returns animation from animations array by object, to which it refers. + */ + public function getAnimationByObject(object:Object):AnimationClip { + for each (var animation:AnimationClip in animations) { + var objects:Array = animation._objects; + if (objects.indexOf(object) >= 0) { + return animation; + } + } + return null; + } + + /** + * Parses and returns animation. + */ + public static function parseAnimation(data:XML):AnimationClip { + + var document:DaeDocument = new DaeDocument(data, 0); + var clip:AnimationClip = new AnimationClip(); + collectAnimation(clip, document.scene.nodes); + return (clip.numTracks > 0) ? clip : null; + } + + /** + * @private + */ + private static function collectAnimation(clip:AnimationClip, nodes:Vector.):void { + for (var i:int = 0, count:int = nodes.length; i < count; i++) { + var node:DaeNode = nodes[i]; + var animation:AnimationClip = node.parseAnimation(); + if (animation != null) { + for (var t:int = 0, numTracks:int = animation.numTracks; t < numTracks; t++) { + var track:Track = animation.getTrackAt(t); + clip.addTrack(track); + } + } else { + clip.addTrack(node.createStaticTransformTrack()); + } + collectAnimation(clip, node.nodes); + } + } + } +} diff --git a/src/alternativa/engine3d/loaders/ParserMaterial.as b/src/alternativa/engine3d/loaders/ParserMaterial.as new file mode 100644 index 0000000..71e2c92 --- /dev/null +++ b/src/alternativa/engine3d/loaders/ParserMaterial.as @@ -0,0 +1,107 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.materials.*; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.ExternalTextureResource; + import alternativa.engine3d.resources.Geometry; + import alternativa.engine3d.resources.TextureResource; + + import avmplus.getQualifiedClassName; + + import flash.utils.Dictionary; + import flash.utils.getDefinitionByName; + + use namespace alternativa3d; + + /** + * A material which is assigned to each object that we got as a parsing result. This material should be treated as a debugging rather than a production one. + * It keeps links to all possible in Alternativa3D maps (such as light map or normal map) but can render only one of them as a diffuse like + * TextureMaterial. To make object that you get after parsing using all these maps you should create a new StandardMaterial + * and pass to them all textures. Then you can assign this material to the object. + * Since ParserMaterial sores only links to textures, you should worry about loading it. You can use TexturesLoader for. + * Can draws a Skin with no more than 41 Joints per surface. See Skin.divide() for more details. + * + * @see alternativa.engine3d.loaders.TexturesLoader + * @see alternativa.engine3d.materials + * @see alternativa.engine3d.objects.Skin#divide() + */ + public class ParserMaterial extends Material { + /** + * List of colors, that can be assigned to each channel instead of texture. Variants: ambient, emission, diffuse, specular, shininess, reflective, transparent, bump. + */ + public var colors:Object; + /** + * List of ExternalTextureResource, that you can load with a TexturesLoader. Keys of objects are names of channels. Variants: ambient, emission, diffuse, specular, shininess, reflective, transparent, bump. + * + * @see alternativa.engine3d.loaders.TexturesLoader + * @see alternativa.engine3d.resources.ExternalTextureResource + */ + public var textures:Object; + /** + * Transparency of material + */ + public var transparency:Number = 0; + /** + * Channel, that will be rendered. Possible options: ambient, emission, diffuse, specular, shininess, reflective, transparent, bump. + */ + public var renderChannel:String = "diffuse"; + + private var textureMaterial:TextureMaterial; + private var fillMaterial:FillMaterial; + + public function ParserMaterial() { + textures = new Object(); + colors = new Object(); + } + + /** + * @private + */ + override alternativa3d function fillResources(resources:Dictionary, resourceType:Class):void { + super.fillResources(resources, resourceType); + for each(var texture:TextureResource in textures) { + if (texture != null && A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(texture)) as Class, resourceType)) { + resources[texture] = true; + } + + } + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, objectRenderPriority:int = -1):void { + var colorO:Object = colors[renderChannel]; + var map:ExternalTextureResource; + if (colorO != null) { + if(fillMaterial == null) { + fillMaterial = new FillMaterial(int(colorO)); + } else { + fillMaterial.color = int(colorO); + } + fillMaterial.collectDraws(camera, surface, geometry, lights, lightsLength, objectRenderPriority); + } else if ((map = textures[renderChannel]) != null) { + if(textureMaterial == null) { + textureMaterial = new TextureMaterial(map); + } else { + textureMaterial.diffuseMap = map; + } + textureMaterial.collectDraws(camera, surface, geometry, lights, lightsLength, objectRenderPriority); + } + } + + } +} diff --git a/src/alternativa/engine3d/loaders/ResourceLoader.as b/src/alternativa/engine3d/loaders/ResourceLoader.as new file mode 100644 index 0000000..5168395 --- /dev/null +++ b/src/alternativa/engine3d/loaders/ResourceLoader.as @@ -0,0 +1,186 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.resources.BitmapTextureResource; + import alternativa.engine3d.resources.ExternalTextureResource; + + import flash.display.BitmapData; + import flash.display.Loader; + import flash.display3D.Context3D; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.textures.CubeTexture; + import flash.display3D.textures.Texture; + import flash.display3D.textures.TextureBase; + import flash.events.Event; + import flash.events.EventDispatcher; + import flash.events.IOErrorEvent; + import flash.events.ProgressEvent; + import flash.events.SecurityErrorEvent; + import flash.geom.Matrix; + import flash.net.URLLoader; + import flash.net.URLLoaderDataFormat; + import flash.net.URLRequest; + import flash.utils.ByteArray; + import flash.utils.Endian; + + use namespace alternativa3d; + + /** + * @private + */ + public class ResourceLoader extends EventDispatcher { + + private var loadSequence:Vector. = new Vector.(); + private var context:Context3D; + private var currentIndex:int = 0; + public var generateMips:Boolean = true; + + private static const atfRegExp:RegExp = new RegExp(/\.atf/i); + + public function ResourceLoader(generateMips:Boolean = true) { + this.generateMips = generateMips; + } + + public function addResource(resource:ExternalTextureResource):void { + loadSequence.push(resource); + } + + public function addResources(resources:Vector.):void { + for each (var resource:ExternalTextureResource in resources) { + loadSequence.push(resource); + } + } + + public function load(context:Context3D):void { + this.context = context; + currentIndex = 0; + loadNext(); + } + + private function loadNext():void { + + if (currentIndex < loadSequence.length) { + var currentResource:ExternalTextureResource = loadSequence[currentIndex]; + if (currentResource.url.match(atfRegExp)) { + var atfLoader:URLLoader = new URLLoader(); + atfLoader.dataFormat = URLLoaderDataFormat.BINARY; + atfLoader.addEventListener(Event.COMPLETE, onATFComplete); + atfLoader.addEventListener(IOErrorEvent.IO_ERROR, onFailed); + atfLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onFailed); + atfLoader.load(new URLRequest(currentResource.url)); + } else { + var bitmapLoader:Loader = new Loader(); + bitmapLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onBitmapLoaded); + bitmapLoader.contentLoaderInfo.addEventListener(IOErrorEvent.DISK_ERROR, onFailed); + bitmapLoader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onFailed); + bitmapLoader.contentLoaderInfo.addEventListener(IOErrorEvent.NETWORK_ERROR, onFailed); + bitmapLoader.contentLoaderInfo.addEventListener(IOErrorEvent.VERIFY_ERROR, onFailed); + bitmapLoader.contentLoaderInfo.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onFailed); + bitmapLoader.load(new URLRequest(currentResource.url)); + } + } else { + dispatchEvent(new Event(Event.COMPLETE)); + } + } + + private function getNearPowerOf2For(size:Number):Number { + if (size && (size - 1)) { + for (var i:int = 11; i > 0; i--) { + if (size >= (1 << i)) { + return 1 << i; + } + } + return 0; + } else { + return size; + } + } + + private function fitTextureToSizeLimits(textureData:BitmapData):BitmapData { + var fittedTextureData:BitmapData = textureData; + var width:Number, height:Number; + width = getNearPowerOf2For(fittedTextureData.width); + height = getNearPowerOf2For(fittedTextureData.height); + if (width != fittedTextureData.width || height != fittedTextureData.height) { + var newBitmap:BitmapData = new BitmapData(width, height, + fittedTextureData.transparent); + var matrix:Matrix = new Matrix(width/fittedTextureData.width, 0, 0, + height/fittedTextureData.height); + newBitmap.draw(fittedTextureData, matrix); + fittedTextureData = newBitmap; + } + return fittedTextureData; + } + + private function onBitmapLoaded(e:Event):void { + var resized:BitmapData = fitTextureToSizeLimits(e.target.content.bitmapData); + var texture:Texture = context.createTexture(resized.width, resized.height, Context3DTextureFormat.BGRA, false); + texture.uploadFromBitmapData(resized, 0); + if (generateMips) { + BitmapTextureResource.createMips(texture, resized); + } + + var currentResource:ExternalTextureResource = loadSequence[currentIndex]; + currentResource.data = texture; + dispatchEvent(new ProgressEvent(ProgressEvent.PROGRESS)); + currentIndex++; + loadNext(); + } + + private function onFailed(e:Event):void { + trace("Failed to load texture :", loadSequence[currentIndex].url); + //dispatchEvent(e); + currentIndex++; + loadNext(); + } + + private function onATFComplete(e:Event):void { + var value:ByteArray = e.target.data; + value.endian = Endian.LITTLE_ENDIAN; + value.position = 6; + var texture:TextureBase + var type:uint = value.readByte(); + var format:String; + switch (type & 0x7F) { + case 0: + format = Context3DTextureFormat.BGRA; + break; + case 1: + format = Context3DTextureFormat.BGRA; + break; + case 2: + format = Context3DTextureFormat.COMPRESSED; + break; + } + + if ((type & ~0x7F) == 0) { + texture = context.createTexture(1 << value.readByte(), 1 << value.readByte(), format, false); + texture.addEventListener("textureReady", onTextureUploaded); + Texture(texture).uploadCompressedTextureFromByteArray(value, 0, true); + } else { + texture = context.createCubeTexture(1 << value.readByte(), format, false); + texture.addEventListener("textureReady", onTextureUploaded); + CubeTexture(texture).uploadCompressedTextureFromByteArray(value, 0, true) + } + } + + private function onTextureUploaded(e:Event):void { + var currentResource:ExternalTextureResource = loadSequence[currentIndex]; + currentResource.data = TextureBase(e.target); + dispatchEvent(new ProgressEvent(ProgressEvent.PROGRESS)); + currentIndex++; + loadNext(); + } + + } +} diff --git a/src/alternativa/engine3d/loaders/TexturesLoader.as b/src/alternativa/engine3d/loaders/TexturesLoader.as new file mode 100644 index 0000000..7b4b642 --- /dev/null +++ b/src/alternativa/engine3d/loaders/TexturesLoader.as @@ -0,0 +1,328 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.loaders.events.TexturesLoaderEvent; + import alternativa.engine3d.resources.BitmapTextureResource; + import alternativa.engine3d.resources.ExternalTextureResource; + + import flash.display.BitmapData; + import flash.display.Loader; + import flash.display3D.Context3D; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.textures.CubeTexture; + import flash.display3D.textures.Texture; + import flash.display3D.textures.TextureBase; + import flash.events.ErrorEvent; + import flash.events.Event; + import flash.events.EventDispatcher; + import flash.events.IOErrorEvent; + import flash.events.SecurityErrorEvent; + import flash.net.URLLoader; + import flash.net.URLLoaderDataFormat; + import flash.net.URLRequest; + import flash.utils.ByteArray; + import flash.utils.Endian; + + use namespace alternativa3d; + + /** + * Dispatches after complete loading of all textures. + * @eventType flash.events.TexturesLoaderEvent.COMPLETE + */ + [Event(name="complete",type="alternativa.engine3d.loaders.events.TexturesLoaderEvent")] + + /** + * An object that downloads textures by their reference and upload them into the Context3D + */ + public class TexturesLoader extends EventDispatcher { + + /** + * A Context3D to which resources wil be loaded. + */ + public var context:Context3D; + + private var textures:Object = new Object(); + private var bitmapDatas:Object = new Object(); + private var byteArrays:Object = new Object(); + + private var currentBitmapDatas:Vector.; + private var currentUrl:String; + + private var resources:Vector.; + private var counter:int = 0; + private var createTexture3D:Boolean; + private var needBitmapData:Boolean; + + private var loaderCompressed:URLLoader; + private var isATF:Boolean; + private var atfRegExp:RegExp = new RegExp(/\.atf/i); + + /** + * Creates a new TexturesLoader instance. + * @param context – A Context3D to which resources wil be loaded. + */ + public function TexturesLoader(context:Context3D) { + this.context = context; + } + + /** + * @private + */ + public function getTexture(url:String):TextureBase { + return textures[url]; + } + + private function loadCompressed(url:String):void { + loaderCompressed = new URLLoader(); + loaderCompressed.dataFormat = URLLoaderDataFormat.BINARY; + loaderCompressed.addEventListener(Event.COMPLETE, loadNext); + loaderCompressed.addEventListener(IOErrorEvent.IO_ERROR, loadNext); + loaderCompressed.addEventListener(SecurityErrorEvent.SECURITY_ERROR, loadNext); + loaderCompressed.load(new URLRequest(url)); + } + + /** + * Loads a resource. + * @param resource + * @param createTexture3D Create texture on uploading + * @param needBitmapData If true, keeps BitmapData after uploading textures into a context. + */ + public function loadResource(resource:ExternalTextureResource, createTexture3D:Boolean = true, needBitmapData:Boolean = true):void { + if (resources != null) { + throw new Error("Cannot start new load while loading"); + } + this.createTexture3D = createTexture3D; + this.needBitmapData = needBitmapData; + resources = Vector.([resource]); + currentBitmapDatas = new Vector.(1); + //currentTextures3D = new Vector.(1); + loadNext(); + } + + /** + * Loads list of textures + * @param resources List of ExternalTextureResource each of them has link to texture file which needs to be downloaded. + * @param createTexture3D Create texture on uploading. + * @param needBitmapData If true, keeps BitmapData after uploading textures into a context. + */ + public function loadResources(resources:Vector., createTexture3D:Boolean = true, needBitmapData:Boolean = true):void { + if (this.resources != null) { + throw new Error("Cannot start new load while loading"); + } + this.createTexture3D = createTexture3D; + this.needBitmapData = needBitmapData; + this.resources = resources; + currentBitmapDatas = new Vector.(resources.length); + loadNext(); + } + + /** + * Clears links to all data stored in this TexturesLoader instance. (List of downloaded textures) + */ + public function clean():void { + if (resources != null) { + throw new Error("Cannot clean while loading"); + } + textures = new Object(); + bitmapDatas = new Object(); + } + + /** + * Clears links to all data stored in this TexturesLoader instance and removes it from the context. + */ + public function cleanAndDispose():void { + if (resources != null) { + throw new Error("Cannot clean while loading"); + } + textures = new Object(); + for each (var b:BitmapData in bitmapDatas) { + b.dispose(); + } + bitmapDatas = new Object(); + } + + /** + * Removes texture resources from Context3D. + * @param urls List of links to resources, that should be removed. + */ + public function dispose(urls:Vector.):void { + for (var i:int = 0; i < urls.length; i++) { + var url:String = urls[i]; + var bmd:BitmapData = bitmapDatas[url] as BitmapData; + //if (bmd) { + delete bitmapDatas[url]; + bmd.dispose(); + //} + } + } + + private function loadNext(e:Event = null):void { +// trace("[NEXT]", e); + var bitmapData:BitmapData; + var byteArray:ByteArray; + var texture3D:TextureBase; + + if (e != null && !(e is ErrorEvent)) { + if (isATF) { + byteArray = e.target.data; + byteArrays[currentUrl] = byteArray; + try { + texture3D = addCompressedTexture(byteArray); + resources[counter - 1].data = texture3D; + } catch (err:Error) { + // throw new Error("loadNext:: " + err.message + " " + currentUrl); + trace("loadNext:: " + err.message + " " + currentUrl); + } + } else { + bitmapData = e.target.content.bitmapData; + bitmapDatas[currentUrl] = bitmapData; + currentBitmapDatas[counter - 1] = bitmapData; + if (createTexture3D) { + try { + texture3D = addTexture(bitmapData); + resources[counter - 1].data = texture3D; + } catch (err:Error) { + throw new Error("loadNext:: " + err.message + " " + currentUrl); + } + } + if (!needBitmapData) { + bitmapData.dispose(); + } + } + resources[counter - 1].data = texture3D; + } else if (e is ErrorEvent) { + trace("Missing: " + currentUrl); + } + + if (counter < resources.length) { + currentUrl = resources[counter++].url; + if (currentUrl != null) { + atfRegExp.lastIndex = currentUrl.lastIndexOf("."); + isATF = currentUrl.match(atfRegExp) != null; + } + + if (isATF) { + if (createTexture3D) { + texture3D = textures[currentUrl]; + if (texture3D == null) { + byteArray = byteArrays[currentUrl]; + if (byteArray) { + texture3D = addCompressedTexture(byteArray); + resources[counter - 1].data = texture3D; + //bitmapDatas[currentUrl] = bitmapData; + loadNext(); + } else { + loadCompressed(currentUrl); + } + } else { + resources[counter - 1].data = texture3D; + loadNext(); + } + } + } else { + if (needBitmapData) { + bitmapData = bitmapDatas[currentUrl]; + if (bitmapData) { + currentBitmapDatas[counter - 1] = bitmapData; + if (createTexture3D) { + texture3D = textures[currentUrl]; + if (texture3D == null) { + texture3D = addTexture(bitmapData); + } + resources[counter - 1].data = texture3D; + } + loadNext(); + } else { + load(currentUrl); + } + } else if (createTexture3D) { + texture3D = textures[currentUrl]; + if (texture3D == null) { + bitmapData = bitmapDatas[currentUrl]; + if (bitmapData) { + texture3D = addTexture(bitmapData); + resources[counter - 1].data = texture3D; + loadNext(); + } else { + load(currentUrl); + } + } else { + resources[counter - 1].data = texture3D; + loadNext(); + } + } + } + } else { + onTexturesLoad(); + } + } + + private function load(url:String):void { + var loader:Loader = new Loader(); + loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loadNext); + loader.contentLoaderInfo.addEventListener(IOErrorEvent.DISK_ERROR, loadNext); + loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, loadNext); + loader.contentLoaderInfo.addEventListener(IOErrorEvent.NETWORK_ERROR, loadNext); + loader.contentLoaderInfo.addEventListener(IOErrorEvent.VERIFY_ERROR, loadNext); + loader.contentLoaderInfo.addEventListener(SecurityErrorEvent.SECURITY_ERROR, loadNext); + loader.load(new URLRequest(url)); + } + + private function onTexturesLoad():void { +// trace("[LOADED]"); + counter = 0; + var bmds:Vector. = currentBitmapDatas; + var reses:Vector. = resources; + currentBitmapDatas = null; + resources = null; + dispatchEvent(new TexturesLoaderEvent(TexturesLoaderEvent.COMPLETE, bmds, reses)); + } + + private function addTexture(value:BitmapData):Texture { + var texture:Texture = context.createTexture(value.width, value.height, Context3DTextureFormat.BGRA, false); + texture.uploadFromBitmapData(value, 0); + BitmapTextureResource.createMips(texture, value); + textures[currentUrl] = texture; + return texture; + } + + private function addCompressedTexture(value:ByteArray):TextureBase { + value.endian = Endian.LITTLE_ENDIAN; + value.position = 6; + var texture:TextureBase + var type:uint = value.readByte(); + var format:String; + switch (type & 0x7F) { + case 0: + format = Context3DTextureFormat.BGRA; + break; + case 1: + format = Context3DTextureFormat.BGRA; + break; + case 2: + format = Context3DTextureFormat.COMPRESSED; + break; + } + if ((type & ~0x7F) == 0) { + texture = context.createTexture(1 << value.readByte(), 1 << value.readByte(), format, false); + Texture(texture).uploadCompressedTextureFromByteArray(value, 0); + } else { + texture = context.createCubeTexture(1 << value.readByte(), format, false); + CubeTexture(texture).uploadCompressedTextureFromByteArray(value, 0) + } + textures[currentUrl] = texture; + return texture; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeArray.as b/src/alternativa/engine3d/loaders/collada/DaeArray.as new file mode 100644 index 0000000..d67c11c --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeArray.as @@ -0,0 +1,51 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + /** + * @private + */ + public class DaeArray extends DaeElement { + + use namespace collada; + + /** + * Array of String values. + * Call parse() before using. + */ + public var array:Array; + + public function DaeArray(data:XML, document:DaeDocument) { + super(data, document); + } + + public function get type():String { + return String(data.localName()); + } + + override protected function parseImplementation():Boolean { + array = parseStringArray(data); + var countXML:XML = data.@count[0]; + if (countXML != null) { + var count:int = parseInt(countXML.toString(), 10); + if (array.length < count) { + document.logger.logNotEnoughDataError(data.@count[0]); + return false; + } else { + array.length = count; + return true; + } + } + return false; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeChannel.as b/src/alternativa/engine3d/loaders/collada/DaeChannel.as new file mode 100644 index 0000000..f99121f --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeChannel.as @@ -0,0 +1,213 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.animation.keys.NumberKey; + import alternativa.engine3d.animation.keys.NumberTrack; + import alternativa.engine3d.animation.keys.Track; + + use namespace alternativa3d; + /** + * @private + */ + public class DaeChannel extends DaeElement { + + static public const PARAM_UNDEFINED:String = "undefined"; + static public const PARAM_TRANSLATE_X:String = "x"; + static public const PARAM_TRANSLATE_Y:String = "y"; + static public const PARAM_TRANSLATE_Z:String = "z"; + static public const PARAM_SCALE_X:String = "scaleX"; + static public const PARAM_SCALE_Y:String = "scaleY"; + static public const PARAM_SCALE_Z:String = "scaleZ"; + static public const PARAM_ROTATION_X:String = "rotationX"; + static public const PARAM_ROTATION_Y:String = "rotationY"; + static public const PARAM_ROTATION_Z:String = "rotationZ"; + static public const PARAM_TRANSLATE:String = "translate"; + static public const PARAM_SCALE:String = "scale"; + static public const PARAM_MATRIX:String = "matrix"; + + /** + * Animation track with keys. + * Call parse() before using. + */ + public var tracks:Vector.; + + /** + * Type of animated parameter. It can be one of DaeChannel.PARAM_*. values. + * * Call parse() before using. + */ + public var animatedParam:String = PARAM_UNDEFINED; + /** + * Key of animated object. + */ + public var animName:String; + + public function DaeChannel(data:XML, document:DaeDocument) { + super(data, document); + } + + /** + * Returns a node for which the animation is destined. + */ + public function get node():DaeNode { + var targetXML:XML = data.@target[0]; + if (targetXML != null) { + var targetParts:Array = targetXML.toString().split("/"); + // First part of item id + var node:DaeNode = document.findNodeByID(targetParts[0]); + if (node != null) { + // Last part is transformed item + targetParts.pop(); + for (var i:int = 1, count:int = targetParts.length; i < count; i++) { + var sid:String = targetParts[i]; + node = node.getNodeBySid(sid); + if (node == null) { + return null; + } + } + return node; + } + } + return null; + } + + override protected function parseImplementation():Boolean { + parseTransformationType(); + parseSampler(); + return true; + } + + private function parseTransformationType():void { + var targetXML:XML = data.@target[0]; + if (targetXML == null) return; + + // Split the path on parts + var targetParts:Array = targetXML.toString().split("/"); + var sid:String = targetParts.pop(); + var sidParts:Array = sid.split("."); + var sidPartsCount:int = sidParts.length; + + //Define the type of property + var transformationXML:XML; + var node:DaeNode = this.node; + if (node == null) { + return; + } + animName = node.animName; + var children:XMLList = node.data.children(); + for (var i:int = 0, count:int = children.length(); i < count; i++) { + var child:XML = children[i]; + var attr:XML = child.@sid[0]; + if (attr != null && attr.toString() == sidParts[0]) { + transformationXML = child; + break; + } + } + // TODO:: case with brackets (just in case) + var transformationName:String = (transformationXML != null) ? transformationXML.localName() as String : null; + if (sidPartsCount > 1) { + var componentName:String = sidParts[1]; + switch (transformationName) { + case "translate": + switch (componentName) { + case "X": + animatedParam = PARAM_TRANSLATE_X; + break; + case "Y": + animatedParam = PARAM_TRANSLATE_Y; + break; + case "Z": + animatedParam = PARAM_TRANSLATE_Z; + break; + } + break; + case "rotate": { + var axis:Array = parseNumbersArray(transformationXML); + // TODO:: look for the maximum value + switch (axis.indexOf(1)) { + case 0: + animatedParam = PARAM_ROTATION_X; + break; + case 1: + animatedParam = PARAM_ROTATION_Y; + break; + case 2: + animatedParam = PARAM_ROTATION_Z; + break; + } + break; + } + case "scale": + switch (componentName) { + case "X": + animatedParam = PARAM_SCALE_X; + break; + case "Y": + animatedParam = PARAM_SCALE_Y; + break; + case "Z": + animatedParam = PARAM_SCALE_Z; + break; + } + break; + } + } else { + switch (transformationName) { + case "translate": + animatedParam = PARAM_TRANSLATE; + break; + case "scale": + animatedParam = PARAM_SCALE; + break; + case "matrix": + animatedParam = PARAM_MATRIX; + break; + } + } + } + + private function parseSampler():void { + var sampler:DaeSampler = document.findSampler(data.@source[0]); + if (sampler != null) { + sampler.parse(); + if (animatedParam == PARAM_MATRIX) { + tracks = Vector.([sampler.parseTransformationTrack(animName)]); + return; + } + if (animatedParam == PARAM_TRANSLATE) { + tracks = sampler.parsePointsTracks(animName, "x", "y", "z"); + return; + } + if (animatedParam == PARAM_SCALE) { + tracks = sampler.parsePointsTracks(animName, "scaleX", "scaleY", "scaleZ"); + return; + } + if (animatedParam == PARAM_ROTATION_X || animatedParam == PARAM_ROTATION_Y || animatedParam == PARAM_ROTATION_Z) { + var track:NumberTrack = sampler.parseNumbersTrack(animName, animatedParam); + // Convert degrees to radians + var toRad:Number = Math.PI/180; + for (var key:NumberKey = track.keyList; key != null; key = key.next) { + key._value *= toRad; + } + tracks = Vector.([track]); + return; + } + if (animatedParam == PARAM_TRANSLATE_X || animatedParam == PARAM_TRANSLATE_Y || animatedParam == PARAM_TRANSLATE_Z || animatedParam == PARAM_SCALE_X || animatedParam == PARAM_SCALE_Y || animatedParam == PARAM_SCALE_Z) { + tracks = Vector.([sampler.parseNumbersTrack(animName, animatedParam)]); + } + } else { + document.logger.logNotFoundError(data.@source[0]); + } + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeController.as b/src/alternativa/engine3d/loaders/collada/DaeController.as new file mode 100644 index 0000000..fb8628c --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeController.as @@ -0,0 +1,542 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import alternativa.engine3d.*; + import alternativa.engine3d.animation.AnimationClip; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.loaders.ParserMaterial; + import alternativa.engine3d.objects.Joint; + import alternativa.engine3d.objects.Skin; + import alternativa.engine3d.resources.Geometry; + + import flash.utils.ByteArray; + import flash.utils.Dictionary; + import flash.utils.Endian; + + use namespace collada; + use namespace alternativa3d; + + /** + * @private + */ + public class DaeController extends DaeElement { + + private var jointsBindMatrices:Vector. >; + private var vcounts:Array; + private var indices:Array; + private var jointsInput:DaeInput; + private var weightsInput:DaeInput; + private var inputsStride:int; + private var geometry:Geometry; + private var primitives:Vector.; + private var maxJointsPerVertex:int = 0; + private var bindShapeMatrix:Vector.; + + public function DaeController(data:XML, document:DaeDocument) { + super(data, document); + + // sources creates inside the DaeDocument. It should not be here. + } + + private function get daeGeometry():DaeGeometry { + var geom:DaeGeometry = document.findGeometry(data.skin.@source[0]); + if (geom == null) { + document.logger.logNotFoundError(data.@source[0]); + } + return geom; + } + + override protected function parseImplementation():Boolean { + var vertexWeightsXML:XML = this.data.skin.vertex_weights[0]; + if (vertexWeightsXML == null) { + return false; + } + var vcountsXML:XML = vertexWeightsXML.vcount[0]; + if (vcountsXML == null) { + return false; + } + vcounts = parseIntsArray(vcountsXML); + var indicesXML:XML = vertexWeightsXML.v[0]; + if (indicesXML == null) { + return false; + } + indices = parseIntsArray(indicesXML); + parseInputs(); + parseJointsBindMatrices(); + var i:int, j:int; + for (i = 0; i < vcounts.length; i++) { + var count:int = vcounts[i]; + if (maxJointsPerVertex < count) maxJointsPerVertex = count; + } + + var geom:DaeGeometry = this.daeGeometry; + bindShapeMatrix = getBindShapeMatrix(); + if (geom != null) { + geom.parse(); + var vertices:Vector. = geom.geometryVertices; + var source:Geometry = geom.geometry; + var localMaxJointsPerVertex:int = (maxJointsPerVertex%2 != 0) ? maxJointsPerVertex + 1 : maxJointsPerVertex; + + // Create geometry + this.geometry = new Geometry(); + this.geometry._indices = source._indices.slice(); + var attributes:Array = source.getVertexStreamAttributes(0); + var numSourceAttributes:int = attributes.length; + + var index:int = numSourceAttributes; + for (i = 0; i < localMaxJointsPerVertex; i += 2) { + var attribute:int = VertexAttributes.JOINTS[int(i/2)]; + attributes[int(index++)] = attribute; + attributes[int(index++)] = attribute; + attributes[int(index++)] = attribute; + attributes[int(index++)] = attribute; + } + + var numMappings:int = attributes.length; + + var sourceData:ByteArray = source._vertexStreams[0].data; + var data:ByteArray = new ByteArray(); + data.endian = Endian.LITTLE_ENDIAN; + data.length = 4*numMappings*source._numVertices; + // Copy source data + sourceData.position = 0; + for (i = 0; i < source._numVertices; i++) { + data.position = 4*numMappings*i; + for (j = 0; j < numSourceAttributes; j++) { + data.writeFloat(sourceData.readFloat()); + } + } + + // Copy weights and joints + var byteArray:ByteArray = createVertexBuffer(vertices, localMaxJointsPerVertex); + byteArray.position = 0; + for (i = 0; i < source._numVertices; i++) { + data.position = 4*(numMappings*i + numSourceAttributes); + for (j = 0; j < localMaxJointsPerVertex; j++) { + data.writeFloat(byteArray.readFloat()); + data.writeFloat(byteArray.readFloat()); + } + } + + this.geometry.addVertexStream(attributes); + this.geometry._vertexStreams[0].data = data; + this.geometry._numVertices = source._numVertices; + transformVertices(this.geometry); + primitives = geom.primitives; + } + return true; + } + + private function transformVertices(geometry:Geometry):void { + var data:ByteArray = geometry._vertexStreams[0].data; + var numMappings:int = geometry._vertexStreams[0].attributes.length; + for (var i:int = 0; i < geometry._numVertices; i++) { + data.position = 4*numMappings*i; + var x:Number = data.readFloat(); + var y:Number = data.readFloat(); + var z:Number = data.readFloat(); + data.position -= 12; + data.writeFloat(x*bindShapeMatrix[0] + y*bindShapeMatrix[1] + z*bindShapeMatrix[2] + bindShapeMatrix[3]); + data.writeFloat(x*bindShapeMatrix[4] + y*bindShapeMatrix[5] + z*bindShapeMatrix[6] + bindShapeMatrix[7]); + data.writeFloat(x*bindShapeMatrix[8] + y*bindShapeMatrix[9] + z*bindShapeMatrix[10] + bindShapeMatrix[11]); + } + } + + private function createVertexBuffer(vertices:Vector., localMaxJointsPerVertex:int):ByteArray { + var jointsOffset:int = jointsInput.offset; + var weightsOffset:int = weightsInput.offset; + var weightsSource:DaeSource = weightsInput.prepareSource(1); + var weights:Vector. = weightsSource.numbers; + var weightsStride:int = weightsSource.stride; + var i:int, count:int; + var verticesDict:Dictionary = new Dictionary(); + var byteArray:ByteArray = new ByteArray(); + // Reserve required number of bytes + byteArray.length = vertices.length*localMaxJointsPerVertex*8; + byteArray.endian = Endian.LITTLE_ENDIAN; + + for (i = 0,count = vertices.length; i < count; i++) { + var vertex:DaeVertex = vertices[i]; + if (vertex == null) continue; + var vec:Vector. = verticesDict[vertex.vertexInIndex]; + if (vec == null) { + vec = verticesDict[vertex.vertexInIndex] = new Vector.(); + } + vec.push(vertex.vertexOutIndex); + } + + var vertexIndex:int = 0; + var vertexOutIndices:Vector.; + for (i = 0,count = vcounts.length; i < count; i++) { + var jointsPerVertex:int = vcounts[i]; + vertexOutIndices = verticesDict[i]; + for (var j:int = 0; j < vertexOutIndices.length; j++) { + byteArray.position = vertexOutIndices[j]*localMaxJointsPerVertex*8; + // Loop on joints + for (var k:int = 0; k < jointsPerVertex; k++) { + var index:int = inputsStride*(vertexIndex + k); + var jointIndex:int = indices[int(index + jointsOffset)]; + if (jointIndex >= 0) { + // Save joint index, multiplied with three + byteArray.writeFloat(jointIndex*3); + var weightIndex:int = indices[int(index + weightsOffset)]; + byteArray.writeFloat(weights[int(weightsStride*weightIndex)]); + } else { + byteArray.position += 8; + } + } + } + vertexIndex += jointsPerVertex; + } + byteArray.position = 0; + return byteArray; + } + + private function parseInputs():void { + var inputsList:XMLList = data.skin.vertex_weights.input; + var maxInputOffset:int = 0; + for (var i:int = 0, count:int = inputsList.length(); i < count; i++) { + var input:DaeInput = new DaeInput(inputsList[i], document); + var semantic:String = input.semantic; + if (semantic != null) { + switch (semantic) { + case "JOINT" : + if (jointsInput == null) { + jointsInput = input; + } + break; + case "WEIGHT" : + if (weightsInput == null) { + weightsInput = input; + } + break; + } + } + var offset:int = input.offset; + maxInputOffset = (offset > maxInputOffset) ? offset : maxInputOffset; + } + inputsStride = maxInputOffset + 1; + } + + /** + * Parses inverse matrices for joints and saves them to a vector. + */ + private function parseJointsBindMatrices():void { + var jointsXML:XML = data.skin.joints.input.(@semantic == "INV_BIND_MATRIX")[0]; + if (jointsXML != null) { + var jointsSource:DaeSource = document.findSource(jointsXML.@source[0]); + if (jointsSource != null) { + if (jointsSource.parse() && jointsSource.numbers != null && jointsSource.stride >= 16) { + var stride:int = jointsSource.stride; + var count:int = jointsSource.numbers.length/stride; + jointsBindMatrices = new Vector. >(count); + for (var i:int = 0; i < count; i++) { + var index:int = stride*i; + var matrix:Vector. = new Vector.(16); + jointsBindMatrices[i] = matrix; + for (var j:int = 0; j < 16; j++) { + matrix[j] = jointsSource.numbers[int(index + j)]; + } + } + } + } else { + document.logger.logNotFoundError(jointsXML.@source[0]); + } + } + } + + /** + * Returns geometry with the joints and controller for the joints. + * Call parse() before using. + */ + public function parseSkin(materials:Object, topmostJoints:Vector., skeletons:Vector.):DaeObject { + var skinXML:XML = data.skin[0]; + if (skinXML != null) { + bindShapeMatrix = getBindShapeMatrix(); + var numJoints:int = jointsBindMatrices.length; + var skin:Skin = new Skin(maxJointsPerVertex); + skin.geometry = this.geometry; + var joints:Vector. = addJointsToSkin(skin, topmostJoints, findNodes(skeletons)); + setJointsBindMatrices(joints); + + skin.renderedJoints = collectRenderedJoints(joints, numJoints); + + if (primitives != null) { + for (var i:int = 0; i < primitives.length; i++) { + var p:DaePrimitive = primitives[i]; + var instanceMaterial:DaeInstanceMaterial = materials[p.materialSymbol]; + var material:ParserMaterial; + if (instanceMaterial != null) { + var daeMaterial:DaeMaterial = instanceMaterial.material; + if (daeMaterial != null) { + daeMaterial.parse(); + material = daeMaterial.material; + daeMaterial.used = true; + } + } + skin.addSurface(material, p.indexBegin, p.numTriangles); + } + } + skin.calculateBoundBox(); + return new DaeObject(skin, mergeJointsClips(skin, joints)); + } + return null; + } + + private function collectRenderedJoints(joints:Vector., numJoints:int):Vector. { + var result:Vector. = new Vector.(); + for (var i:int = 0; i < numJoints; i++) { + result[i] = Joint(joints[i].object); + } + return result; + } + + /** + * Unites animations of joints into the single animation, if required. + */ + private function mergeJointsClips(skin:Skin, joints:Vector.):AnimationClip { + if (!hasJointsAnimation(joints)) { + return null; + } + var result:AnimationClip = new AnimationClip(); + var resultObjects:Array = [skin]; + for (var i:int = 0, count:int = joints.length; i < count; i++) { + var animatedObject:DaeObject = joints[i]; + var clip:AnimationClip = animatedObject.animation; + if (clip != null) { + for (var t:int = 0; t < clip.numTracks; t++) { + result.addTrack(clip.getTrackAt(t)); + } + } else { + // Creates empty track for the joint. + result.addTrack(animatedObject.jointNode.createStaticTransformTrack()); + } + var object:Object3D = animatedObject.object; + object.name = animatedObject.jointNode.animName; + resultObjects.push(object); + } + result._objects = resultObjects; + return result; + } + + private function hasJointsAnimation(joints:Vector.):Boolean { + for (var i:int = 0, count:int = joints.length; i < count; i++) { + var object:DaeObject = joints[i]; + if (object.animation != null) { + return true; + } + } + return false; + } + + /** + * Set inverse matrices to joints. + */ + private function setJointsBindMatrices(animatedJoints:Vector.):void { + for (var i:int = 0, count:int = jointsBindMatrices.length; i < count; i++) { + var animatedJoint:DaeObject = animatedJoints[i]; + var bindMatrix:Vector. = jointsBindMatrices[i]; +// bindMatrix[3]; //*= document.unitScaleFactor; +// bindMatrix[7];// *= document.unitScaleFactor; +// bindMatrix[11];// *= document.unitScaleFactor; + Joint(animatedJoint.object).setBindPoseMatrix(bindMatrix); + } + } + + /** + * Creates a hierarchy of joints and adds them to skin. + * @return vector of joints with animation, which was added to skin. + * If you have added the auxiliary joint, then length of vector will differ from length of nodes vector. + */ + private function addJointsToSkin(skin:Skin, topmostJoints:Vector., nodes:Vector.):Vector. { + // Dictionary, in which key is a node and value is a position in nodes vector + var nodesDictionary:Dictionary = new Dictionary(); + var count:int = nodes.length; + var i:int; + for (i = 0; i < count; i++) { + nodesDictionary[nodes[i]] = i; + } + var animatedJoints:Vector. = new Vector.(count); + var numTopmostJoints:int = topmostJoints.length; + for (i = 0; i < numTopmostJoints; i++) { + var topmostJoint:DaeNode = topmostJoints[i]; + var animatedJoint:DaeObject = addRootJointToSkin(skin, topmostJoint, animatedJoints, nodesDictionary); + addJointChildren(Joint(animatedJoint.object), animatedJoints, topmostJoint, nodesDictionary); + } + return animatedJoints; + } + + /** + * Adds root joint to skin. + */ + private function addRootJointToSkin(skin:Skin, node:DaeNode, animatedJoints:Vector., nodes:Dictionary):DaeObject { + var joint:Joint = new Joint(); + joint.name = cloneString(node.name); + skin.addChild(joint); + var animatedJoint:DaeObject = node.applyAnimation(node.applyTransformations(joint)); + animatedJoint.jointNode = node; + if (node in nodes) { + animatedJoints[nodes[node]] = animatedJoint; + } else { + // Add at the end + animatedJoints.push(animatedJoint); + } + return animatedJoint; + } + + /** + * Creates a hierarchy of child joints and add them to parent joint. + * @param parent Parent joint. + * @param animatedJoints Vector of joints to which created joints will added. + * Auxiliary joints will be added to the end of the vector, if it's necessary. + * @param parentNode Node of parent joint + * @param nodes Dictionary. Key is a node of joint. And value is an index of joint in animatedJoints vector + * + */ + private function addJointChildren(parent:Joint, animatedJoints:Vector., parentNode:DaeNode, nodes:Dictionary):void { + var object:DaeObject; + var children:Vector. = parentNode.nodes; + for (var i:int = 0, count:int = children.length; i < count; i++) { + var child:DaeNode = children[i]; + var joint:Joint; + if (child in nodes) { + joint = new Joint(); + joint.name = cloneString(child.name); + object = child.applyAnimation(child.applyTransformations(joint)); + object.jointNode = child; + animatedJoints[nodes[child]] = object; + parent.addChild(joint); + addJointChildren(joint, animatedJoints, child, nodes); + } else { + // If node is not a joint + if (hasJointInDescendants(child, nodes)) { + // If one of the children is a joint, there is need to create auxiliary joint instead of this node. + joint = new Joint(); + joint.name = cloneString(child.name); + object = child.applyAnimation(child.applyTransformations(joint)); + object.jointNode = child; + // Add new joint to the end + animatedJoints.push(object); + parent.addChild(joint); + addJointChildren(joint, animatedJoints, child, nodes); + } + } + } + } + + private function hasJointInDescendants(parentNode:DaeNode, nodes:Dictionary):Boolean { + var children:Vector. = parentNode.nodes; + for (var i:int = 0, count:int = children.length; i < count; i++) { + var child:DaeNode = children[i]; + if (child in nodes || hasJointInDescendants(child, nodes)) { + return true; + } + } + return false; + } + + /** + * Transforms all object vertices with the BindShapeMatrix from collada. + */ + private function getBindShapeMatrix():Vector. { + var matrixXML:XML = data.skin.bind_shape_matrix[0]; + var res:Vector. = new Vector.(16, true); + if (matrixXML != null) { + var matrix:Array = parseStringArray(matrixXML); + for (var i:int = 0; i < matrix.length; i++) { + res[i] = Number(matrix[i]); + } + } + return res; + } + + /** + * Returns true if joint hasn't parent joint. + * @param node Joint node + * @param nodes Dictionary. It items are the nodes keys. + * + */ + private function isRootJointNode(node:DaeNode, nodes:Dictionary):Boolean { + for (var parent:DaeNode = node.parent; parent != null; parent = parent.parent) { + if (parent in nodes) { + return false; + } + } + return true; + } + + public function findRootJointNodes(skeletons:Vector.):Vector. { + var nodes:Vector. = findNodes(skeletons); + var i:int = 0; + var count:int = nodes.length; + if (count > 0) { + var nodesDictionary:Dictionary = new Dictionary(); + for (i = 0; i < count; i++) { + nodesDictionary[nodes[i]] = i; + } + var rootNodes:Vector. = new Vector.(); + for (i = 0; i < count; i++) { + var node:DaeNode = nodes[i]; + if (isRootJointNode(node, nodesDictionary)) { + rootNodes.push(node); + } + } + return rootNodes; + } + return null; + } + + /** + * Find node by Sid on sceletons vector. + */ + private function findNode(nodeName:String, skeletons:Vector.):DaeNode { + var count:int = skeletons.length; + for (var i:int = 0; i < count; i++) { + var node:DaeNode = skeletons[i].getNodeBySid(nodeName); + if (node != null) { + return node; + } + } + return null; + } + + /** + * Returns the vector of joint nodes. + */ + private function findNodes(skeletons:Vector.):Vector. { + var jointsXML:XML = data.skin.joints.input.(@semantic == "JOINT")[0]; + if (jointsXML != null) { + var jointsSource:DaeSource = document.findSource(jointsXML.@source[0]); + if (jointsSource != null) { + if (jointsSource.parse() && jointsSource.names != null) { + var stride:int = jointsSource.stride; + var count:int = jointsSource.names.length/stride; + var nodes:Vector. = new Vector.(count); + for (var i:int = 0; i < count; i++) { + var node:DaeNode = findNode(jointsSource.names[int(stride*i)], skeletons); + if (node == null) { + // Error: no node. + } + nodes[i] = node; + } + return nodes; + } + } else { + document.logger.logNotFoundError(jointsXML.@source[0]); + } + } + return null; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeDocument.as b/src/alternativa/engine3d/loaders/collada/DaeDocument.as new file mode 100644 index 0000000..b0d77c0 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeDocument.as @@ -0,0 +1,248 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + /** + * @private + */ + public class DaeDocument { + + use namespace collada; + + public var scene:DaeVisualScene; + + /** + * Collada file + */ + private var data:XML; + + // Dictionaries to store matchings id-> DaeElement + internal var sources:Object; + internal var arrays:Object; + internal var vertices:Object; + public var geometries:Object; + internal var nodes:Object; + internal var lights:Object; + internal var images:Object; + internal var effects:Object; + internal var controllers:Object; + internal var samplers:Object; + + public var materials:Object; + + internal var logger:DaeLogger; + + public var versionMajor:uint; + public var versionMinor:uint; + public var unitScaleFactor:Number = 1; + public function DaeDocument(document:XML, units:Number) { + this.data = document; + + var versionComponents:Array = data.@version[0].toString().split(/[.,]/); + versionMajor = parseInt(versionComponents[1], 10); + versionMinor = parseInt(versionComponents[2], 10); + + var colladaUnit:Number = parseFloat(data.asset[0].unit[0].@meter); + if (units > 0) { + unitScaleFactor = colladaUnit/units; + } else { + unitScaleFactor = 1; + } + + logger = new DaeLogger(); + + constructStructures(); + constructScenes(); + registerInstanceControllers(); + constructAnimations(); + } + + private function getLocalID(url:XML):String { + var path:String = url.toString(); + if (path.charAt(0) == "#") { + return path.substr(1); + } else { + logger.logExternalError(url); + return null; + } + } + + // Search for the declarations of items and fill the dictionaries. + private function constructStructures():void { + var element:XML; + + sources = new Object(); + arrays = new Object(); + for each (element in data..source) { + // Collect all . Dictionary arrays is filled at constructors. + var source:DaeSource = new DaeSource(element, this); + if (source.id != null) { + sources[source.id] = source; + } + } + + lights = new Object(); + for each (element in data.library_lights.light) { + // Collect all . + var light:DaeLight = new DaeLight(element, this); + if (light.id != null) { + lights[light.id] = light; + } + } + images = new Object(); + for each (element in data.library_images.image) { + // Collect all . + var image:DaeImage = new DaeImage(element, this); + if (image.id != null) { + images[image.id] = image; + } + } + effects = new Object(); + for each (element in data.library_effects.effect) { + // Collect all . Dictionary images is filled at constructors. + var effect:DaeEffect = new DaeEffect(element, this); + if (effect.id != null) { + effects[effect.id] = effect; + } + } + materials = new Object(); + for each (element in data.library_materials.material) { + // Collect all . + var material:DaeMaterial = new DaeMaterial(element, this); + if (material.id != null) { + materials[material.id] = material; + } + } + geometries = new Object(); + vertices = new Object(); + for each (element in data.library_geometries.geometry) { + // Collect all . Dictionary vertices is filled at constructors. + var geom:DaeGeometry = new DaeGeometry(element, this); + if (geom.id != null) { + geometries[geom.id] = geom; + } + } + + controllers = new Object(); + for each (element in data.library_controllers.controller) { + // Collect all . + var controller:DaeController = new DaeController(element, this); + if (controller.id != null) { + controllers[controller.id] = controller; + } + } + + nodes = new Object(); + for each (element in data.library_nodes.node) { + // Create only root nodes. Others are created recursively at constructors. + var node:DaeNode = new DaeNode(element, this); + if (node.id != null) { + nodes[node.id] = node; + } + } + } + + private function constructScenes():void { + var vsceneURL:XML = data.scene.instance_visual_scene.@url[0]; + var vsceneID:String = getLocalID(vsceneURL); + for each (var element:XML in data.library_visual_scenes.visual_scene) { + // Create visual_scene. node's are created at constructors. + var vscene:DaeVisualScene = new DaeVisualScene(element, this); + if (vscene.id == vsceneID) { + this.scene = vscene; + } + } + if (vsceneID != null && scene == null) { + logger.logNotFoundError(vsceneURL); + } + } + + private function registerInstanceControllers():void { + if (scene != null) { + for (var i:int = 0, count:int = scene.nodes.length; i < count; i++) { + scene.nodes[i].registerInstanceControllers(); + } + } + } + + private function constructAnimations():void { + var element:XML; + samplers = new Object(); + for each (element in data.library_animations..sampler) { + // Collect all . + var sampler:DaeSampler = new DaeSampler(element, this); + if (sampler.id != null) { + samplers[sampler.id] = sampler; + } + } + + for each (element in data.library_animations..channel) { + var channel:DaeChannel = new DaeChannel(element, this); + var node:DaeNode = channel.node; + if (node != null) { + node.addChannel(channel); + } + } + } + + public function findArray(url:XML):DaeArray { + return arrays[getLocalID(url)]; + } + + public function findSource(url:XML):DaeSource { + return sources[getLocalID(url)]; + } + + public function findLight(url:XML):DaeLight { + return lights[getLocalID(url)]; + } + + public function findImage(url:XML):DaeImage { + return images[getLocalID(url)]; + } + + public function findImageByID(id:String):DaeImage { + return images[id]; + } + + public function findEffect(url:XML):DaeEffect { + return effects[getLocalID(url)]; + } + + public function findMaterial(url:XML):DaeMaterial { + return materials[getLocalID(url)]; + } + + public function findVertices(url:XML):DaeVertices { + return vertices[getLocalID(url)]; + } + + public function findGeometry(url:XML):DaeGeometry { + return geometries[getLocalID(url)]; + } + + public function findNode(url:XML):DaeNode { + return nodes[getLocalID(url)]; + } + + public function findNodeByID(id:String):DaeNode { + return nodes[id]; + } + + public function findController(url:XML):DaeController { + return controllers[getLocalID(url)]; + } + + public function findSampler(url:XML):DaeSampler { + return samplers[getLocalID(url)]; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeEffect.as b/src/alternativa/engine3d/loaders/collada/DaeEffect.as new file mode 100644 index 0000000..c358cf6 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeEffect.as @@ -0,0 +1,215 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import alternativa.engine3d.loaders.ParserMaterial; + import alternativa.engine3d.resources.ExternalTextureResource; + + /** + * @private + */ + public class DaeEffect extends DaeElement { + + public static var commonAlways:Boolean = false; + + use namespace collada; + + private var effectParams:Object; + private var commonParams:Object; + private var techniqueParams:Object; + + private var diffuse:DaeEffectParam; + private var ambient:DaeEffectParam; + private var transparent:DaeEffectParam; + private var transparency:DaeEffectParam; + private var bump:DaeEffectParam; + private var reflective:DaeEffectParam; + private var emission:DaeEffectParam; + private var specular:DaeEffectParam; + + public function DaeEffect(data:XML, document:DaeDocument) { + super(data, document); + + // image's are declared at + constructImages(); + } + + private function constructImages():void { + var list:XMLList = data..image; + for each (var element:XML in list) { + var image:DaeImage = new DaeImage(element, document); + if (image.id != null) { + document.images[image.id] = image; + } + } + } + + override protected function parseImplementation():Boolean { + var element:XML; + var param:DaeParam; + effectParams = new Object(); + for each (element in data.newparam) { + param = new DaeParam(element, document); + effectParams[param.sid] = param; + } + commonParams = new Object(); + for each (element in data.profile_COMMON.newparam) { + param = new DaeParam(element, document); + commonParams[param.sid] = param; + } + techniqueParams = new Object(); + var technique:XML = data.profile_COMMON.technique[0]; + if (technique != null) { + for each (element in technique.newparam) { + param = new DaeParam(element, document); + techniqueParams[param.sid] = param; + } + } + var shader:XML = data.profile_COMMON.technique.*.(localName() == "constant" || localName() == "lambert" || localName() == "phong" || localName() == "blinn")[0]; + if (shader != null) { + var diffuseXML:XML = null; + if (shader.localName() == "constant") { + diffuseXML = shader.emission[0]; + } else { + diffuseXML = shader.diffuse[0]; + var emissionXML:XML = shader.emission[0]; + if (emissionXML != null) { + emission = new DaeEffectParam(emissionXML, this); + } + } + if (diffuseXML != null) { + diffuse = new DaeEffectParam(diffuseXML, this); + } + if (shader.localName() == "phong" || shader.localName() == "blinn") { + var specularXML:XML = shader.specular[0]; + if (specularXML != null) { + specular = new DaeEffectParam(specularXML, this); + } + } + var transparentXML:XML = shader.transparent[0]; + if (transparentXML != null) { + transparent = new DaeEffectParam(transparentXML, this); + } + var transparencyXML:XML = shader.transparency[0]; + if (transparencyXML != null) { + transparency = new DaeEffectParam(transparencyXML, this); + } + var ambientXML:XML = shader.ambient[0]; + if(ambientXML != null) { + ambient = new DaeEffectParam(ambientXML, this); + } + var reflectiveXML:XML = shader.reflective[0]; + if(reflectiveXML != null) { + reflective = new DaeEffectParam(reflectiveXML, this); + } + } + var bumpXML:XML = data.profile_COMMON.technique.extra.technique.(hasOwnProperty("@profile") && @profile == "OpenCOLLADA3dsMax").bump[0]; + if (bumpXML != null) { + bump = new DaeEffectParam(bumpXML, this); + } + return true; + } + + internal function getParam(name:String, setparams:Object):DaeParam { + var param:DaeParam = setparams[name]; + if (param != null) { + return param; + } + param = techniqueParams[name]; + if (param != null) { + return param; + } + param = commonParams[name]; + if (param != null) { + return param; + } + return effectParams[name]; + } + + private function float4ToUint(value:Array, alpha:Boolean = true):uint { + var r:uint = (value[0] * 255); + var g:uint = (value[1] * 255); + var b:uint = (value[2] * 255); + if (alpha) { + var a:uint = (value[3] * 255); + return (a << 24) | (r << 16) | (g << 8) | b; + } else { + return (r << 16) | (g << 8) | b; + } + } + + /** + * Returns material of the engine with given parameters. + * Call parse() before using. + */ + public function getMaterial(setparams:Object):ParserMaterial { + if (diffuse != null) { + var material:ParserMaterial = new ParserMaterial(); + if (diffuse) { + pushMap(material, diffuse, setparams); + } + if (specular != null) { + pushMap(material, specular, setparams); + } + + if (emission != null) { + pushMap(material, emission, setparams); + } + if (transparency != null) { + material.transparency = transparency.getFloat(setparams); + } + if (transparent != null) { + pushMap(material, transparent, setparams); + } + if (bump != null) { + pushMap(material, bump, setparams); + } + if (ambient) { + pushMap(material, ambient, setparams); + } + if (reflective) { + pushMap(material, reflective, setparams); + } + return material; + } + return null; + } + + private function pushMap(material:ParserMaterial, param:DaeEffectParam, setparams:Object):void { + var color:Array = param.getColor(setparams); + + if(color != null){ + material.colors[cloneString(param.data.localName())] = float4ToUint(color, true); + } + else { + var image:DaeImage = param.getImage(setparams); + if(image != null){ + material.textures[cloneString(param.data.localName())] = new ExternalTextureResource(cloneString(image.init_from)); + } + } + } + + /** + * Name of texture channel for main map of object. + * Call parse() before using. + */ + public function get mainTexCoords():String { + var channel:String = null; + channel = (channel == null && diffuse != null) ? diffuse.texCoord : channel; + channel = (channel == null && transparent != null) ? transparent.texCoord : channel; + channel = (channel == null && bump != null) ? bump.texCoord : channel; + channel = (channel == null && emission != null) ? emission.texCoord : channel; + channel = (channel == null && specular != null) ? specular.texCoord : channel; + return channel; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeEffectParam.as b/src/alternativa/engine3d/loaders/collada/DaeEffectParam.as new file mode 100644 index 0000000..fc17aeb --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeEffectParam.as @@ -0,0 +1,95 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + /** + * @private + */ + public class DaeEffectParam extends DaeElement { + + use namespace collada; + + private var effect:DaeEffect; + + public function DaeEffectParam(data:XML, effect:DaeEffect) { + super(data, effect.document); + this.effect = effect; + } + + public function getFloat(setparams:Object):Number { + var floatXML:XML = data.float[0]; + if (floatXML != null) { + return parseNumber(floatXML); + } + var paramRef:XML = data.param.@ref[0]; + if (paramRef != null) { + var param:DaeParam = effect.getParam(paramRef.toString(), setparams); + if (param != null) { + return param.getFloat(); + } + } + return NaN; + } + + public function getColor(setparams:Object):Array { + var colorXML:XML = data.color[0]; + if (colorXML != null) { + return parseNumbersArray(colorXML); + } + var paramRef:XML = data.param.@ref[0]; + if (paramRef != null) { + var param:DaeParam = effect.getParam(paramRef.toString(), setparams); + if (param != null) { + return param.getFloat4(); + } + } + return null; + } + + private function get texture():String { + var attr:XML = data.texture.@texture[0]; + return (attr == null) ? null : attr.toString(); + } + + public function getSampler(setparams:Object):DaeParam { + var sid:String = texture; + if (sid != null) { + return effect.getParam(sid, setparams); + } + return null; + } + + public function getImage(setparams:Object):DaeImage { + var sampler:DaeParam = getSampler(setparams); + if (sampler != null) { + var surfaceSID:String = sampler.surfaceSID; + if (surfaceSID != null) { + var surface:DaeParam = effect.getParam(surfaceSID, setparams); + if (surface != null) { + return surface.image; + } + } else { + return sampler.image; + } + } else { + // case of 3ds mas default export or so was used, it ignores spec and srores direct link to image + return document.findImageByID(texture); + } + return null; + } + + public function get texCoord():String { + var attr:XML = data.texture.@texcoord[0]; + return (attr == null) ? null : attr.toString(); + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeElement.as b/src/alternativa/engine3d/loaders/collada/DaeElement.as new file mode 100644 index 0000000..95b4961 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeElement.as @@ -0,0 +1,115 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + import flash.utils.ByteArray; + + /** + * @private + */ + public class DaeElement { + + use namespace collada; + + public var document:DaeDocument; + + public var data:XML; + + /** + * -1 - not parsed, 0 - parsed with error, 1 - parsed without error. + */ + private var _parsed:int = -1; + + private static var _byteArray:ByteArray = new ByteArray(); + + public function DaeElement(data:XML, document:DaeDocument) { + this.document = document; + this.data = data; + } + + public function cloneString(str:String):String { + if(str == null) return null; + _byteArray.position = 0; + _byteArray.writeUTF(str); + _byteArray.position = 0; + var res:String = _byteArray.readUTF(); + return res; + } + + /** + * Performs pre-setting of object. + * @return false on error. + */ + public function parse():Boolean { + // -1 - not parsed, 0 - parsed with error, 1 - parsed without error. + if (_parsed < 0) { + _parsed = parseImplementation() ? 1 : 0; + return _parsed != 0; + } + return _parsed != 0; + } + + /** + * Overridden method parse(). + */ + protected function parseImplementation():Boolean { + return true; + } + + /** + * Returns array of String values. + */ + protected function parseStringArray(element:XML):Array { + return element.text().toString().split(/\s+/); + } + + protected function parseNumbersArray(element:XML):Array { + var arr:Array = element.text().toString().split(/\s+/); + for (var i:int = 0, count:int = arr.length; i < count; i++) { + var value:String = arr[i]; + if (value.indexOf(",") != -1) { + value = value.replace(/,/, "."); + } + arr[i] = parseFloat(value); + } + return arr; + } + + protected function parseIntsArray(element:XML):Array { + var arr:Array = element.text().toString().split(/\s+/); + for (var i:int = 0, count:int = arr.length; i < count; i++) { + var value:String = arr[i]; + arr[i] = parseInt(value, 10); + } + return arr; + } + + protected function parseNumber(element:XML):Number { + var value:String = element.toString().replace(/,/, "."); + return parseFloat(value); + } + + public function get id():String { + var idXML:XML = data.@id[0]; + return (idXML == null) ? null : idXML.toString(); + } + + public function get sid():String { + var attr:XML = data.@sid[0]; + return (attr == null) ? null : attr.toString(); + } + + public function get name():String { + var nameXML:XML = data.@name[0]; + return (nameXML == null) ? null : nameXML.toString(); + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeGeometry.as b/src/alternativa/engine3d/loaders/collada/DaeGeometry.as new file mode 100644 index 0000000..c923adb --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeGeometry.as @@ -0,0 +1,187 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.loaders.ParserMaterial; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.resources.Geometry; + + import flash.utils.ByteArray; + import flash.utils.Endian; + + /** + * @private + */ + public class DaeGeometry extends DaeElement { + + use namespace collada; + use namespace alternativa3d; + + internal var geometryVertices:Vector.; + internal var primitives:Vector.; + internal var geometry:Geometry; + + private var vertices:DaeVertices; + + public function DaeGeometry(data:XML, document:DaeDocument) { + super(data, document); + + /** + * Items sources, vertices are declared in the . + * You should create sources in DaeDocument, not here. + */ + constructVertices(); + } + + private function constructVertices():void { + var verticesXML:XML = data.mesh.vertices[0]; + if (verticesXML != null) { + vertices = new DaeVertices(verticesXML, document); + document.vertices[vertices.id] = vertices; + } + } + + override protected function parseImplementation():Boolean { + if (vertices != null) { + parsePrimitives(); + + vertices.parse(); + var numVertices:int = vertices.positions.numbers.length/vertices.positions.stride; + geometry = new Geometry(); + geometryVertices = new Vector.(numVertices); + var i:int; + var p:DaePrimitive; + var channels:uint = 0; + for (i = 0; i < primitives.length; i++) { + p = primitives[i]; + p.parse(); + if (p.verticesEquals(vertices)) { + numVertices = geometryVertices.length; + channels |= p.fillGeometry(geometry, geometryVertices); + } else { + // Error, Vertices of another geometry can not be used + } + } + + var attributes:Array = new Array(3); + attributes[0] = VertexAttributes.POSITION; + attributes[1] = VertexAttributes.POSITION; + attributes[2] = VertexAttributes.POSITION; + var index:int = 3; + if (channels & DaePrimitive.NORMALS) { + attributes[index++] = VertexAttributes.NORMAL; + attributes[index++] = VertexAttributes.NORMAL; + attributes[index++] = VertexAttributes.NORMAL; + } + if (channels & DaePrimitive.TANGENT4) { + attributes[index++] = VertexAttributes.TANGENT4; + attributes[index++] = VertexAttributes.TANGENT4; + attributes[index++] = VertexAttributes.TANGENT4; + attributes[index++] = VertexAttributes.TANGENT4; + } + for (i = 0; i < 8; i++) { + if (channels & DaePrimitive.TEXCOORDS[i]) { + attributes[index++] = VertexAttributes.TEXCOORDS[i]; + attributes[index++] = VertexAttributes.TEXCOORDS[i]; + } + } + + geometry.addVertexStream(attributes); + + numVertices = geometryVertices.length; + + var data:ByteArray = new ByteArray(); + data.endian = Endian.LITTLE_ENDIAN; + + var numMappings:int = attributes.length; + data.length = 4*numMappings*numVertices; + + for (i = 0; i < numVertices; i++) { + var vertex:DaeVertex = geometryVertices[i]; + if (vertex != null) { + data.position = 4*numMappings*i; + data.writeFloat(vertex.x); + data.writeFloat(vertex.y); + data.writeFloat(vertex.z); + if (vertex.normal != null) { + data.writeFloat(vertex.normal.x); + data.writeFloat(vertex.normal.y); + data.writeFloat(vertex.normal.z); + } + if (vertex.tangent != null) { + data.writeFloat(vertex.tangent.x); + data.writeFloat(vertex.tangent.y); + data.writeFloat(vertex.tangent.z); + data.writeFloat(vertex.tangent.w); + } + for (var j:int = 0; j < vertex.uvs.length; j++) { + data.writeFloat(vertex.uvs[j]); + } + } + } + geometry._vertexStreams[0].data = data; + geometry._numVertices = numVertices; + return true; + } + return false; + } + + private function parsePrimitives():void { + primitives = new Vector.(); + var children:XMLList = data.mesh.children(); + + for (var i:int = 0, count:int = children.length(); i < count; i++) { + var child:XML = children[i]; + switch (child.localName()) { + case "polygons": + case "polylist": + case "triangles": + case "trifans": + case "tristrips": + var p:DaePrimitive = new DaePrimitive(child, document); + primitives.push(p); + break; + } + } + } + + /** + * Creates geometry and returns it as mesh. + * Call parse() before using. + * @param materials Dictionary of materials + */ + public function parseMesh(materials:Object):Mesh { + if (data.mesh.length() > 0) { + var mesh:Mesh = new Mesh(); + mesh.geometry = geometry; + for (var i:int = 0; i < primitives.length; i++) { + var p:DaePrimitive = primitives[i]; + var instanceMaterial:DaeInstanceMaterial = materials[p.materialSymbol]; + var material:ParserMaterial; + if (instanceMaterial != null) { + var daeMaterial:DaeMaterial = instanceMaterial.material; + if (daeMaterial != null) { + daeMaterial.parse(); + material = daeMaterial.material; + daeMaterial.used = true; + } + } + mesh.addSurface(material, p.indexBegin, p.numTriangles); + } + mesh.calculateBoundBox(); + return mesh; + } + return null; + } + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeImage.as b/src/alternativa/engine3d/loaders/collada/DaeImage.as new file mode 100644 index 0000000..6c8640f --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeImage.as @@ -0,0 +1,37 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + /** + * @private + */ + public class DaeImage extends DaeElement { + + use namespace collada; + + public function DaeImage(data:XML, document:DaeDocument) { + super(data, document); + } + + public function get init_from():String { + var element:XML = data.init_from[0]; + if (element != null) { + if (document.versionMajor > 4) { + var refXML:XML = element.ref[0]; + return (refXML == null) ? null : refXML.text().toString(); + } + return element.text().toString(); + } + return null; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeInput.as b/src/alternativa/engine3d/loaders/collada/DaeInput.as new file mode 100644 index 0000000..69a0ad6 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeInput.as @@ -0,0 +1,63 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + /** + * @private + */ + public class DaeInput extends DaeElement { + + use namespace collada; + + public function DaeInput(data:XML, document:DaeDocument) { + super(data, document); + } + + public function get semantic():String { + var attribute:XML = data.@semantic[0]; + return (attribute == null) ? null : attribute.toString(); + } + + public function get source():XML { + return data.@source[0]; + } + + public function get offset():int { + var attr:XML = data.@offset[0]; + return (attr == null) ? 0 : parseInt(attr.toString(), 10); + } + + public function get setNum():int { + var attr:XML = data.@set[0]; + return (attr == null) ? 0 : parseInt(attr.toString(), 10); + } + + /** + * If DaeSource, located at the link source, is type of Number and + * number of components is not less than specified number, then this method will return it. + * + */ + public function prepareSource(minComponents:int):DaeSource { + var source:DaeSource = document.findSource(this.source); + if (source != null) { + source.parse(); + if (source.numbers != null && source.stride >= minComponents) { + return source; + } else { + } + } else { + document.logger.logNotFoundError(data.@source[0]); + } + return null; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeInstanceController.as b/src/alternativa/engine3d/loaders/collada/DaeInstanceController.as new file mode 100644 index 0000000..6a87456 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeInstanceController.as @@ -0,0 +1,111 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import flash.utils.Dictionary; + + /** + * @private + */ + public class DaeInstanceController extends DaeElement { + + use namespace collada; + + public var node:DaeNode; + + /** + * List of top-level joints, which have common parent. (List of top-level joints, that have the common parent) + * Call parse() befire using. + */ + public var topmostJoints:Vector.; + + public function DaeInstanceController(data:XML, document:DaeDocument, node:DaeNode) { + super(data, document); + this.node = node; + } + + override protected function parseImplementation():Boolean { + var controller:DaeController = this.controller; + if (controller != null) { + topmostJoints = controller.findRootJointNodes(this.skeletons); + if (topmostJoints != null && topmostJoints.length > 1) { + replaceNodesByTopmost(topmostJoints); + } + } + return topmostJoints != null; + } + + /** + * Replaces each node in the list with its parent (the parent must be the same for all others node's or be a scene) + * @param nodes not empty array of nodes. + */ + private function replaceNodesByTopmost(nodes:Vector.):void { + var i:int; + var node:DaeNode, parent:DaeNode; + var numNodes:int = nodes.length; + var parents:Dictionary = new Dictionary(); + for (i = 0; i < numNodes; i++) { + node = nodes[i]; + for (parent = node.parent; parent != null; parent = parent.parent) { + if (parents[parent]) { + parents[parent]++; + } else { + parents[parent] = 1; + } + } + } + // Replase node with its parent if it has the same parent with each other node or has no parent at all + for (i = 0; i < numNodes; i++) { + node = nodes[i]; + while ((parent = node.parent) != null && (parents[parent] != numNodes)) { + node = node.parent; + } + nodes[i] = node; + } + } + + private function get controller():DaeController { + var controller:DaeController = document.findController(data.@url[0]); + if (controller == null) { + document.logger.logNotFoundError(data.@url[0]); + } + return controller; + } + + private function get skeletons():Vector. { + var list:XMLList = data.skeleton; + if (list.length() > 0) { + var skeletons:Vector. = new Vector.(); + for (var i:int = 0, count:int = list.length(); i < count; i++) { + var skeletonXML:XML = list[i]; + var skel:DaeNode = document.findNode(skeletonXML.text()[0]); + if (skel != null) { + skeletons.push(skel); + } else { + document.logger.logNotFoundError(skeletonXML); + } + } + return skeletons; + } + return null; + } + + public function parseSkin(materials:Object):DaeObject { + var controller:DaeController = this.controller; + if (controller != null) { + controller.parse(); + return controller.parseSkin(materials, topmostJoints, this.skeletons); + } + return null; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeInstanceMaterial.as b/src/alternativa/engine3d/loaders/collada/DaeInstanceMaterial.as new file mode 100644 index 0000000..213e955 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeInstanceMaterial.as @@ -0,0 +1,49 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + /** + * @private + */ + public class DaeInstanceMaterial extends DaeElement { + + use namespace collada; + + public function DaeInstanceMaterial(data:XML, document:DaeDocument) { + super(data, document); + } + + public function get symbol():String { + var attribute:XML = data.@symbol[0]; + return (attribute == null) ? null : attribute.toString(); + } + + private function get target():XML { + return data.@target[0]; + } + + public function get material():DaeMaterial { + var mat:DaeMaterial = document.findMaterial(target); + if (mat == null) { + document.logger.logNotFoundError(target); + } + return mat; + } + + public function getBindVertexInputSetNum(semantic:String):int { + var bindVertexInputXML:XML = data.bind_vertex_input.(@semantic == semantic)[0]; + if (bindVertexInputXML == null) return 0; + var setNumXML:XML = bindVertexInputXML.@input_set[0]; + return (setNumXML == null) ? 0 : parseInt(setNumXML.toString(), 10); + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeLight.as b/src/alternativa/engine3d/loaders/collada/DaeLight.as new file mode 100644 index 0000000..b15ede4 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeLight.as @@ -0,0 +1,119 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.lights.AmbientLight; + import alternativa.engine3d.lights.DirectionalLight; + import alternativa.engine3d.lights.OmniLight; + import alternativa.engine3d.lights.SpotLight; + + /** + * @private + */ + public class DaeLight extends DaeElement { + + use namespace collada; + + public function DaeLight(data:XML, document:DaeDocument) { + super(data, document); + } + + private function float4ToUint(value:Array):uint { + var r:uint = (value[0] * 255); + var g:uint = (value[1] * 255); + var b:uint = (value[2] * 255); + return (r << 16) | (g << 8) | b | 0xFF000000; + } + + public function get revertDirection():Boolean { + var info:XML = data.technique_common.children()[0]; + return (info == null) ? false : (info.localName() == "directional" || info.localName() == "spot"); + } + + public function parseLight():Light3D { + var info:XML = data.technique_common.children()[0]; + var extra:XML = data.extra.technique.(@profile[0] == "OpenCOLLADA3dsMax").light[0]; + var light:Light3D = null; + if (info != null) { + var color:uint = float4ToUint(parseNumbersArray(info.color[0])); + var constantAttenuationXML:XML; + var linearAttenuationXML:XML; + var linearAttenuation:Number = 0; + var attenuationStart:Number = 0; + var attenuationEnd:Number = 1; + switch (info.localName()) { + case "ambient": + light = new AmbientLight(color); + break; + case "directional": + var dLight:DirectionalLight = new DirectionalLight(color); + light = dLight; + break; + case "point": + if (extra != null) { + attenuationStart = parseNumber(extra.attenuation_far_start[0]); + attenuationEnd = parseNumber(extra.attenuation_far_end[0]); + } else { + constantAttenuationXML = info.constant_attenuation[0]; + linearAttenuationXML = info.linear_attenuation[0]; + if (constantAttenuationXML != null) { + attenuationStart = -parseNumber(constantAttenuationXML); + } + if (linearAttenuationXML != null) { + linearAttenuation = parseNumber(linearAttenuationXML); + } + if (linearAttenuation > 0) { + attenuationEnd = 1/linearAttenuation + attenuationStart; + } else { + attenuationEnd = attenuationStart + 1; + } + } + var oLight:OmniLight = new OmniLight(color, attenuationStart, attenuationEnd); + light = oLight; + break; + case "spot": + var hotspot:Number = 0; + var fallof:Number = Math.PI/4; + const DEG2RAD:Number = Math.PI/180; + if (extra != null) { + attenuationStart = parseNumber(extra.attenuation_far_start[0]); + attenuationEnd = parseNumber(extra.attenuation_far_end[0]); + hotspot = DEG2RAD * parseNumber(extra.hotspot_beam[0]); + fallof = DEG2RAD * parseNumber(extra.falloff[0]); + } else { + constantAttenuationXML = info.constant_attenuation[0]; + linearAttenuationXML = info.linear_attenuation[0]; + if (constantAttenuationXML != null) { + attenuationStart = -parseNumber(constantAttenuationXML); + } + if (linearAttenuationXML != null) { + linearAttenuation = parseNumber(linearAttenuationXML); + } + if (linearAttenuation > 0) { + attenuationEnd = 1/linearAttenuation + attenuationStart; + } else { + attenuationEnd = attenuationStart + 1; + } + } + var sLight:SpotLight = new SpotLight(color, attenuationStart, attenuationEnd, hotspot, fallof); + light = sLight; + break; + } + } + if (extra != null) { + light.intensity = parseNumber(extra.multiplier[0]); + } + return light; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeLogger.as b/src/alternativa/engine3d/loaders/collada/DaeLogger.as new file mode 100644 index 0000000..e693fd5 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeLogger.as @@ -0,0 +1,62 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + /** + * @private + */ + public class DaeLogger { + + public function DaeLogger() { + } + + private function logMessage(message:String, element:XML):void { + var index:int = 0; + var name:String = (element.nodeKind() == "attribute") ? "@" + element.localName() : element.localName() + ((index > 0) ? "[" + index + "]" : ""); + var parent:* = element.parent(); + while (parent != null) { + // index = parent.childIndex(); + name = parent.localName() + ((index > 0) ? "[" + index + "]" : "") + "." + name; + parent = parent.parent(); + } + trace(message, '| "' + name + '"'); + } + + private function logError(message:String, element:XML):void { + logMessage("[ERROR] " + message, element); + } + + public function logExternalError(element:XML):void { + logError("External urls don't supported", element); + } + + public function logSkewError(element:XML):void { + logError(" don't supported", element); + } + + public function logJointInAnotherSceneError(element:XML):void { + logError("Joints in different scenes don't supported", element); + } + + public function logInstanceNodeError(element:XML):void { + logError(" don't supported", element); + } + + public function logNotFoundError(element:XML):void { + logError("Element with url \"" + element.toString() + "\" not found", element); + } + + public function logNotEnoughDataError(element:XML):void { + logError("Not enough data", element); + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeMaterial.as b/src/alternativa/engine3d/loaders/collada/DaeMaterial.as new file mode 100644 index 0000000..f8f7b44 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeMaterial.as @@ -0,0 +1,72 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import alternativa.engine3d.loaders.ParserMaterial; + + /** + * @private + */ + public class DaeMaterial extends DaeElement { + + use namespace collada; + + /** + * Material of engine. + * Call parse() before using. + */ + public var material:ParserMaterial; + + /** + * Name of texture channel for color map of object. + * Call parse() before using. + */ + public var mainTexCoords:String; + + /** + * If truematerial is in use. + */ + public var used:Boolean = false; + + public function DaeMaterial(data:XML, document:DaeDocument) { + super(data, document); + } + + private function parseSetParams():Object { + var params:Object = new Object(); + var list:XMLList = data.instance_effect.setparam; + for each (var element:XML in list) { + var param:DaeParam = new DaeParam(element, document); + params[param.ref] = param; + } + return params; + } + + private function get effectURL():XML { + return data.instance_effect.@url[0]; + } + + override protected function parseImplementation():Boolean { + var effect:DaeEffect = document.findEffect(effectURL); + if (effect != null) { + effect.parse(); + material = effect.getMaterial(parseSetParams()); + mainTexCoords = effect.mainTexCoords; + if (material != null) { + material.name = cloneString(name); + } + return true; + } + return false; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeNode.as b/src/alternativa/engine3d/loaders/collada/DaeNode.as new file mode 100644 index 0000000..8ea9b79 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeNode.as @@ -0,0 +1,517 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import alternativa.engine3d.animation.AnimationClip; + import alternativa.engine3d.animation.keys.NumberTrack; + import alternativa.engine3d.animation.keys.Track; + import alternativa.engine3d.animation.keys.TransformTrack; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.objects.Skin; + + import flash.geom.Matrix3D; + import flash.geom.Vector3D; + + /** + * @private + */ + public class DaeNode extends DaeElement { + + use namespace collada; + + public var scene:DaeVisualScene; + public var parent:DaeNode; + + // Skin or top-level joint. + public var skinOrTopmostJoint:Boolean = false; + + /** + * Animation channels of this node. + */ + private var channels:Vector.; + + /** + * Vector of controllers, which have reference to this node. + */ + private var instanceControllers:Vector.; + + /** + * Array of nodes at this node. + */ + public var nodes:Vector.; + + /** + * Array of objects at this node. + * Call parse() before using. + * + */ + public var objects:Vector.; + + /** + * Vector of skins at this node. + * Call parse() before using. + * + */ + public var skins:Vector.; + + /** + * Name of object for animation + */ + public function get animName():String { + var n:String = this.name; + return (n == null) ? this.id : n; + } + + /** + * Create node from xml. Child nodes are created recursively. + */ + public function DaeNode(data:XML, document:DaeDocument, scene:DaeVisualScene = null, parent:DaeNode = null) { + super(data, document); + + this.scene = scene; + this.parent = parent; + + // Others node's declares inside + constructNodes(); + } + + private function constructNodes():void { + var nodesList:XMLList = data.node; + var count:int = nodesList.length(); + nodes = new Vector.(count); + for (var i:int = 0; i < count; i++) { + var node:DaeNode = new DaeNode(nodesList[i], document, scene, this); + if (node.id != null) { + document.nodes[node.id] = node; + } + nodes[i] = node; + } + } + + internal function registerInstanceControllers():void { + var instanceControllerXMLs:XMLList = data.instance_controller; + var i:int; + var count:int = instanceControllerXMLs.length() + for (i = 0; i < count; i++) { + skinOrTopmostJoint = true; + var instanceControllerXML:XML = instanceControllerXMLs[i]; + var instanceController:DaeInstanceController = new DaeInstanceController(instanceControllerXML, document, this); + if (instanceController.parse()) { + var jointNodes:Vector. = instanceController.topmostJoints; + var numNodes:int = jointNodes.length; + if (numNodes > 0) { + var jointNode:DaeNode = jointNodes[0]; + jointNode.addInstanceController(instanceController); + for (var j:int = 0; j < numNodes; j++) { + jointNodes[j].skinOrTopmostJoint = true; + } + } + } + } + count = nodes.length; + for (i = 0; i < count; i++) { + nodes[i].registerInstanceControllers(); + } + } + + public function addChannel(channel:DaeChannel):void { + if (channels == null) { + channels = new Vector.(); + } + channels.push(channel); + } + + public function addInstanceController(controller:DaeInstanceController):void { + if (instanceControllers == null) { + instanceControllers = new Vector.(); + } + instanceControllers.push(controller); + } + + override protected function parseImplementation():Boolean { + this.skins = parseSkins(); + this.objects = parseObjects(); + return true; + } + + private function parseInstanceMaterials(geometry:XML):Object { + var instances:Object = new Object(); + var list:XMLList = geometry.bind_material.technique_common.instance_material; + for (var i:int = 0, count:int = list.length(); i < count; i++) { + var instance:DaeInstanceMaterial = new DaeInstanceMaterial(list[i], document); + instances[instance.symbol] = instance; + } + return instances; + } + + /** + * Returns node by Sid. + */ + public function getNodeBySid(sid:String):DaeNode { + if (sid == this.sid) { + return this; + } + + var levelNodes:Vector. > = new Vector. >; + var levelNodes2:Vector. > = new Vector. >; + + levelNodes.push(nodes); + var len:int = levelNodes.length; + while (len > 0) { + for (var i:int = 0; i < len; i++) { + var children:Vector. = levelNodes[i]; + var count:int = children.length; + for (var j:int = 0; j < count; j++) { + var node:DaeNode = children[j]; + if (node.sid == sid) { + return node; + } + if (node.nodes.length > 0) { + levelNodes2.push(node.nodes); + } + } + } + var temp:Vector. > = levelNodes; + levelNodes = levelNodes2; + levelNodes2 = temp; + levelNodes2.length = 0; + + len = levelNodes.length; + } + return null; + } + + /** + * Parses and returns array of skins, associated with this node. + */ + public function parseSkins():Vector. { + if (instanceControllers == null) { + return null; + } + var skins:Vector. = new Vector.(); + for (var i:int = 0, count:int = instanceControllers.length; i < count; i++) { + var instanceController:DaeInstanceController = instanceControllers[i]; + instanceController.parse(); + var skinAndAnimatedJoints:DaeObject = instanceController.parseSkin(parseInstanceMaterials(instanceController.data)); + if (skinAndAnimatedJoints != null) { + var skin:Skin = Skin(skinAndAnimatedJoints.object); + // Name is got from node, that contains instance_controller. + skin.name = cloneString(instanceController.node.name); + // Not apply transformation and animation for skin. It specifies at root joints. + skins.push(skinAndAnimatedJoints); + } + } + return (skins.length > 0) ? skins : null; + } + + /** + * Parses and returns array of objects, associated with this node. + * Can be Mesh or Object3D, if type of object is unknown. + */ + public function parseObjects():Vector. { + var objects:Vector. = new Vector.(); + var children:XMLList = data.children(); + var i:int, count:int; + + for (i = 0, count = children.length(); i < count; i++) { + var child:XML = children[i]; + switch (child.localName()) { + case "instance_light": + var lightInstance:DaeLight = document.findLight(child.@url[0]); + if (lightInstance != null) { + var light:Light3D = lightInstance.parseLight(); + if (light != null) { + light.name = cloneString(name); + if (lightInstance.revertDirection) { + // Rotate 180 degrees along the x-axis, for correspondence to engine + var rotXMatrix:Matrix3D = new Matrix3D(); + rotXMatrix.appendRotation(180, Vector3D.X_AXIS); + // Not upload animations yet for these light sources + objects.push(new DaeObject(applyTransformations(light, rotXMatrix))); + } else { + objects.push(applyAnimation(applyTransformations(light))); + } + } + } else { + document.logger.logNotFoundError(child.@url[0]); + } + break; + case "instance_geometry": + var geom:DaeGeometry = document.findGeometry(child.@url[0]); + if (geom != null) { + geom.parse(); + var mesh:Mesh = geom.parseMesh(parseInstanceMaterials(child)); + if(mesh != null){ + mesh.name = cloneString(name); + objects.push(applyAnimation(applyTransformations(mesh))); + } + } else { + document.logger.logNotFoundError(child.@url[0]); + } + break; + case "instance_node": + document.logger.logInstanceNodeError(child); + break; + } + } + return (objects.length > 0) ? objects : null; + } + + /** + * Returns transformation of node as a matrix. + * @param initialMatrix To this matrix tranformation will appended. + */ + private function getMatrix(initialMatrix:Matrix3D = null):Matrix3D { + var matrix:Matrix3D = (initialMatrix == null) ? new Matrix3D() : initialMatrix; + var components:Array; + var children:XMLList = data.children(); + for (var i:int = children.length() - 1; i >= 0; i--) { + //Transformations are append from the end to begin + var child:XML = children[i]; + var sid:XML = child.@sid[0]; + if (sid != null && sid.toString() == "post-rotationY") { + // Default 3dsmax exporter writes some trash which ignores + continue; + } + switch (child.localName()) { + case "scale" : { + components = parseNumbersArray(child); + matrix.appendScale(components[0], components[1], components[2]); + break; + } + case "rotate" : { + components = parseNumbersArray(child); + matrix.appendRotation(components[3], new Vector3D(components[0], components[1], components[2])); + break; + } + case "translate" : { + components = parseNumbersArray(child); + matrix.appendTranslation(components[0]*document.unitScaleFactor, + components[1]*document.unitScaleFactor, components[2]*document.unitScaleFactor); + break; + } + case "matrix" : { + components = parseNumbersArray(child); + matrix.append(new Matrix3D(Vector.([components[0], components[4], components[8], components[12], + components[1], components[5], components[9], components[13], + components[2], components[6], components[10], components[14], + components[3]*document.unitScaleFactor ,components[7]*document.unitScaleFactor, components[11]*document.unitScaleFactor, components[15]]))); + break; + } + case "lookat" : { + break; + } + case "skew" : { + document.logger.logSkewError(child); + break; + } + } + } + return matrix; + } + + /** + * Apply transformation to object. + * @param prepend If is not null transformation will added to this matrix. + */ + public function applyTransformations(object:Object3D, prepend:Matrix3D = null, append:Matrix3D = null):Object3D { + var matrix:Matrix3D = getMatrix(prepend); + if (append != null) { + matrix.append(append); + } + var vs:Vector. = matrix.decompose(); + var t:Vector3D = vs[0]; + var r:Vector3D = vs[1]; + var s:Vector3D = vs[2]; + object.x = t.x; + object.y = t.y; + object.z = t.z; + object.rotationX = r.x; + object.rotationY = r.y; + object.rotationZ = r.z; + object.scaleX = s.x; + object.scaleY = s.y; + object.scaleZ = s.z; + return object; + } + + public function applyAnimation(object:Object3D):DaeObject { + var animation:AnimationClip = parseAnimation(object); + if (animation == null) { + return new DaeObject(object); + } + object.name = animName; + animation.attach(object, false); + return new DaeObject(object, animation); + } + + /** + * Returns animation of node. + */ + public function parseAnimation(object:Object3D = null):AnimationClip { + if (channels == null || !hasTransformationAnimation()) { + return null; + } + var channel:DaeChannel = getChannel(DaeChannel.PARAM_MATRIX); + if (channel != null) { + return createClip(channel.tracks); + } + var clip:AnimationClip = new AnimationClip(); + var components:Vector. = (object != null) ? null : getMatrix().decompose(); + + // Translation + channel = getChannel(DaeChannel.PARAM_TRANSLATE); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + channel = getChannel(DaeChannel.PARAM_TRANSLATE_X); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + clip.addTrack(createValueStaticTrack("x", (object == null) ? components[0].x : object.x)); + } + channel = getChannel(DaeChannel.PARAM_TRANSLATE_Y); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + clip.addTrack(createValueStaticTrack("y", (object == null) ? components[0].y : object.y)); + } + channel = getChannel(DaeChannel.PARAM_TRANSLATE_Z); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + clip.addTrack(createValueStaticTrack("z", (object == null) ? components[0].z : object.z)); + } + } + // Rotation + channel = getChannel(DaeChannel.PARAM_ROTATION_X); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + clip.addTrack(createValueStaticTrack("rotationX", (object == null) ? components[1].x : object.rotationX)); + } + channel = getChannel(DaeChannel.PARAM_ROTATION_Y); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + clip.addTrack(createValueStaticTrack("rotationY", (object == null) ? components[1].y : object.rotationY)); + } + channel = getChannel(DaeChannel.PARAM_ROTATION_Z); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + clip.addTrack(createValueStaticTrack("rotationZ", (object == null) ? components[1].z : object.rotationZ)); + } + // Scale + channel = getChannel(DaeChannel.PARAM_SCALE); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + channel = getChannel(DaeChannel.PARAM_SCALE_X); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + clip.addTrack(createValueStaticTrack("scaleX", (object == null) ? components[2].x : object.scaleX)); + } + channel = getChannel(DaeChannel.PARAM_SCALE_Y); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + clip.addTrack(createValueStaticTrack("scaleY", (object == null) ? components[2].y : object.scaleY)); + } + channel = getChannel(DaeChannel.PARAM_SCALE_Z); + if (channel != null) { + addTracksToClip(clip, channel.tracks); + } else { + clip.addTrack(createValueStaticTrack("scaleZ", (object == null) ? components[2].z : object.scaleZ)); + } + } + if (clip.numTracks > 0) { + return clip; + } + return null; + } + + private function createClip(tracks:Vector.):AnimationClip { + var clip:AnimationClip = new AnimationClip(); + for (var i:int = 0, count:int = tracks.length; i < count; i++) { + clip.addTrack(tracks[i]); + } + return clip; + } + + private function addTracksToClip(clip:AnimationClip, tracks:Vector.):void { + for (var i:int = 0, count:int = tracks.length; i < count; i++) { + clip.addTrack(tracks[i]); + } + } + + private function hasTransformationAnimation():Boolean { + for (var i:int = 0, count:int = channels.length; i < count; i++) { + var channel:DaeChannel = channels[i]; + channel.parse(); + var result:Boolean = channel.animatedParam == DaeChannel.PARAM_MATRIX; + result ||= channel.animatedParam == DaeChannel.PARAM_TRANSLATE; + result ||= channel.animatedParam == DaeChannel.PARAM_TRANSLATE_X; + result ||= channel.animatedParam == DaeChannel.PARAM_TRANSLATE_Y; + result ||= channel.animatedParam == DaeChannel.PARAM_TRANSLATE_Z; + result ||= channel.animatedParam == DaeChannel.PARAM_ROTATION_X; + result ||= channel.animatedParam == DaeChannel.PARAM_ROTATION_Y; + result ||= channel.animatedParam == DaeChannel.PARAM_ROTATION_Z; + result ||= channel.animatedParam == DaeChannel.PARAM_SCALE; + result ||= channel.animatedParam == DaeChannel.PARAM_SCALE_X; + result ||= channel.animatedParam == DaeChannel.PARAM_SCALE_Y; + result ||= channel.animatedParam == DaeChannel.PARAM_SCALE_Z; + if (result) { + return true; + } + } + return false; + } + + private function getChannel(param:String):DaeChannel { + for (var i:int = 0, count:int = channels.length; i < count; i++) { + var channel:DaeChannel = channels[i]; + channel.parse(); + if (channel.animatedParam == param) { + return channel; + } + } + return null; + } + + private function concatTracks(source:Vector., dest:Vector.):void { + for (var i:int = 0, count:int = source.length; i < count; i++) { + dest.push(source[i]); + } + } + + private function createValueStaticTrack(property:String, value:Number):Track { + var track:NumberTrack = new NumberTrack(animName, property); + track.addKey(0, value); + return track; + } + + public function createStaticTransformTrack():TransformTrack { + var track:TransformTrack = new TransformTrack(animName); + track.addKey(0, getMatrix()); + return track; + } + + public function get layer():String { + var layerXML:XML = data.@layer[0]; + return (layerXML == null) ? null : layerXML.toString(); + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeObject.as b/src/alternativa/engine3d/loaders/collada/DaeObject.as new file mode 100644 index 0000000..5e0468a --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeObject.as @@ -0,0 +1,31 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import alternativa.engine3d.animation.AnimationClip; + import alternativa.engine3d.core.Object3D; + + /** + * @private + */ + public class DaeObject { + + public var object:Object3D; + public var animation:AnimationClip; + public var jointNode:DaeNode; + + public function DaeObject(object:Object3D, animation:AnimationClip = null) { + this.object = object; + this.animation = animation; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeParam.as b/src/alternativa/engine3d/loaders/collada/DaeParam.as new file mode 100644 index 0000000..4a21524 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeParam.as @@ -0,0 +1,89 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + /** + * @private + */ + public class DaeParam extends DaeElement { + + use namespace collada; + + public function DaeParam(data:XML, document:DaeDocument) { + super(data, document); + } + + public function get ref():String { + var attribute:XML = data.@ref[0]; + return (attribute == null) ? null : attribute.toString(); + } + + public function getFloat():Number { + var floatXML:XML = data.float[0]; + if (floatXML != null) { + return parseNumber(floatXML); + } + return NaN; + } + + public function getFloat4():Array { + var element:XML = data.float4[0]; + var components:Array; + if (element == null) { + element = data.float3[0]; + if (element != null) { + components = parseNumbersArray(element); + components[3] = 1.0; + } + } else { + components = parseNumbersArray(element); + } + return components; + } + + /** + * Returns Sid of a parameter type of surface. Only for sampler2D and Collada ver. 1.4 + */ + public function get surfaceSID():String { + var element:XML = data.sampler2D.source[0]; + return (element == null) ? null : element.text().toString(); + } + + public function get wrap_s():String { + var element:XML = data.sampler2D.wrap_s[0]; + return (element == null) ? null : element.text().toString(); + } + + public function get image():DaeImage { + var surface:XML = data.surface[0]; + var image:DaeImage; + if (surface != null) { + // Collada 1.4 + var init_from:XML = surface.init_from[0]; + if (init_from == null) { + // Error + return null; + } + image = document.findImageByID(init_from.text().toString()); + } else { + // Collada 1.5 + var imageIDXML:XML = data.instance_image.@url[0]; + if (imageIDXML == null) { + // error + return null; + } + image = document.findImage(imageIDXML); + } + return image; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaePrimitive.as b/src/alternativa/engine3d/loaders/collada/DaePrimitive.as new file mode 100644 index 0000000..277b255 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaePrimitive.as @@ -0,0 +1,331 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.resources.Geometry; + + use namespace collada; + + use namespace alternativa3d; + + /** + * @private + */ + public class DaePrimitive extends DaeElement { + + internal static const NORMALS:int = 1; + internal static const TANGENT4:int = 2; + internal static const TEXCOORDS:Vector. = Vector.([8, 16, 32, 64, 128, 256, 512, 1024]); + + internal var verticesInput:DaeInput; + internal var texCoordsInputs:Vector.; + internal var normalsInput:DaeInput; + internal var biNormalsInputs:Vector.; + internal var tangentsInputs:Vector.; + + internal var indices:Vector.; + internal var inputsStride:int; + + public var indexBegin:int; + public var numTriangles:int; + + public function DaePrimitive(data:XML, document:DaeDocument) { + super(data, document); + } + + override protected function parseImplementation():Boolean { + parseInputs(); + parseIndices(); + return true; + } + + private function get type():String { + return data.localName() as String; + } + + private function parseInputs():void { + texCoordsInputs = new Vector.(); + tangentsInputs = new Vector.(); + biNormalsInputs = new Vector.(); + var inputsList:XMLList = data.input; + var maxInputOffset:int = 0; + for (var i:int = 0, count:int = inputsList.length(); i < count; i++) { + var input:DaeInput = new DaeInput(inputsList[i], document); + var semantic:String = input.semantic; + if (semantic != null) { + switch (semantic) { + case "VERTEX" : + if (verticesInput == null) { + verticesInput = input; + } + break; + case "TEXCOORD" : + texCoordsInputs.push(input); + break; + case "NORMAL": + if (normalsInput == null) { + normalsInput = input; + } + break; + case "TEXTANGENT": + tangentsInputs.push(input); + break; + case "TEXBINORMAL": + biNormalsInputs.push(input); + break; + } + } + var offset:int = input.offset; + maxInputOffset = (offset > maxInputOffset) ? offset : maxInputOffset; + } + inputsStride = maxInputOffset + 1; + } + + private function parseIndices():void { + indices = new Vector.(); + var array:Array; + var vcount:Vector. = new Vector.(); + var i:int = 0; + var count:int = 0; + switch (data.localName()) { + case "polylist": + case "polygons": + var vcountXML:XMLList = data.vcount; + array = parseStringArray(vcountXML[0]); + for (i = 0,count = array.length; i < count; i++) { + vcount.push(parseInt(array[i])); + } + case "triangles": + var pList:XMLList = data.p; + for (i = 0,count = pList.length(); i < count; i++) { + array = parseStringArray(pList[i]); + for (var j:int = 0; j < array.length; j++) { + indices.push(parseInt(array[j], 10)); + } + if (vcount.length > 0) { + indices = triangulate(indices, vcount); + } + + } + break; + + } + } + + private function triangulate(input:Vector., vcount:Vector.):Vector. { + var res:Vector. = new Vector.(); + var indexIn:uint, indexOut:uint = 0; + var i:int, j:int, k:int, count:int; + for (i = 0,count = vcount.length; i < count; i++) { + var verticesCount:int = vcount[i]; + var attributesCount:int = verticesCount*inputsStride; + if (verticesCount == 3) { + for (j = 0; j < attributesCount; j++,indexIn++,indexOut++) { + res[indexOut] = input[indexIn]; + } + } else { + for (j = 1; j < verticesCount - 1; j++) { + // 0 - vertex + for (k = 0; k < inputsStride; k++,indexOut++) { + res[indexOut] = input[int(indexIn + k)]; + } + // 1 - vertex + for (k = 0; k < inputsStride; k++,indexOut++) { + res[indexOut] = input[int(indexIn + inputsStride*j + k)]; + } + // 2 - vertex + for (k = 0; k < inputsStride; k++,indexOut++) { + res[indexOut] = input[int(indexIn + inputsStride*(j + 1) + k)]; + } + } + indexIn += inputsStride*verticesCount; + } + } + return res; + } + + public function fillGeometry(geometry:Geometry, vertices:Vector.):uint { + if (verticesInput == null) { + // Error + return 0; + } + verticesInput.parse(); + + var numIndices:int = indices.length; + + var daeVertices:DaeVertices = document.findVertices(verticesInput.source); + if (daeVertices == null) { + document.logger.logNotFoundError(verticesInput.source); + return 0; + } + daeVertices.parse(); + + var positionSource:DaeSource = daeVertices.positions; + var vertexStride:int = 3; // XYZ + + var mainSource:DaeSource = positionSource; + var mainInput:DaeInput = verticesInput; + + var tangentSource:DaeSource; + var binormalSource:DaeSource; + + var channels:uint = 0; + var normalSource:DaeSource; + var inputOffsets:Vector. = new Vector.(); + inputOffsets.push(verticesInput.offset); + if (normalsInput != null) { + normalSource = normalsInput.prepareSource(3); + inputOffsets.push(normalsInput.offset); + vertexStride += 3; + channels |= NORMALS; + if (tangentsInputs.length > 0 && biNormalsInputs.length > 0) { + tangentSource = tangentsInputs[0].prepareSource(3); + inputOffsets.push(tangentsInputs[0].offset); + binormalSource = biNormalsInputs[0].prepareSource(3); + inputOffsets.push(biNormalsInputs[0].offset); + vertexStride += 4; + channels |= TANGENT4; + } + } + var textureSources:Vector. = new Vector.(); + var numTexCoordsInputs:int = texCoordsInputs.length; + if (numTexCoordsInputs > 8) { + // TODO: Warning + numTexCoordsInputs = 8; + } + for (var i:int = 0; i < numTexCoordsInputs; i++) { + var s:DaeSource = texCoordsInputs[i].prepareSource(2); + textureSources.push(s); + inputOffsets.push(texCoordsInputs[i].offset); + vertexStride += 2; + channels |= TEXCOORDS[i]; + } + + var verticesLength:int = vertices.length; + + // Make geometry data + var index:uint; + var vertex:DaeVertex; + + indexBegin = geometry._indices.length; + for (i = 0; i < numIndices; i += inputsStride) { + index = indices[int(i + mainInput.offset)]; + + vertex = vertices[index]; + if (vertex == null || !isEqual(vertex, indices, i, inputOffsets)) { + if (vertex != null) { + // Add to end + index = verticesLength++; + } + vertex = new DaeVertex(); + vertices[index] = vertex; + vertex.vertexInIndex = indices[int(i + verticesInput.offset)]; + vertex.addPosition(positionSource.numbers, vertex.vertexInIndex, positionSource.stride, document.unitScaleFactor); + + if (normalSource != null) { + vertex.addNormal(normalSource.numbers, indices[int(i + normalsInput.offset)], normalSource.stride); + + } + if (tangentSource != null) { + vertex.addTangentBiDirection(tangentSource.numbers, indices[int(i + tangentsInputs[0].offset)], tangentSource.stride, binormalSource.numbers, indices[int(i + biNormalsInputs[0].offset)], binormalSource.stride); + } + for (var j:int = 0; j < textureSources.length; j++) { + vertex.appendUV(textureSources[j].numbers, indices[int(i + texCoordsInputs[j].offset)], textureSources[j].stride); + } + } + vertex.vertexOutIndex = index; + geometry._indices.push(index); + } + numTriangles = (geometry._indices.length - indexBegin)/3; + return channels; + } + + private function isEqual(vertex:DaeVertex, indices:Vector., index:int, offsets:Vector.):Boolean { + var numOffsets:int = offsets.length; + for (var j:int = 0; j < numOffsets; j++) { + if (vertex.indices[j] != indices[int(index + offsets[j])]) { + return false; + } + } + return true; + } + + private function findInputBySet(inputs:Vector., setNum:int):DaeInput { + for (var i:int = 0, numInputs:int = inputs.length; i < numInputs; i++) { + var input:DaeInput = inputs[i]; + if (input.setNum == setNum) { + return input; + } + } + return null; + } + + /** + * Returns array of texture channels data. First element stores channel with mainSetNum. + */ + private function getTexCoordsDatas(mainSetNum:int):Vector. { + var mainInput:DaeInput = findInputBySet(texCoordsInputs, mainSetNum); + var i:int; + var numInputs:int = texCoordsInputs.length; + var datas:Vector. = new Vector.(); + for (i = 0; i < numInputs; i++) { + var texCoordsInput:DaeInput = texCoordsInputs[i]; + var texCoordsSource:DaeSource = texCoordsInput.prepareSource(2); + if (texCoordsSource != null) { + var data:VertexChannelData = new VertexChannelData(texCoordsSource.numbers, texCoordsSource.stride, texCoordsInput.offset, texCoordsInput.setNum); + if (texCoordsInput == mainInput) { + datas.unshift(data); + } else { + datas.push(data); + } + } + } + return (datas.length > 0) ? datas : null; + } + + /** + * Compare vertices of the privitive with given at otherVertices parameter vertices. + * Call parse() before using. + */ + public function verticesEquals(otherVertices:DaeVertices):Boolean { + var vertices:DaeVertices = document.findVertices(verticesInput.source); + if (vertices == null) { + document.logger.logNotFoundError(verticesInput.source); + } + return vertices == otherVertices; + } + + public function get materialSymbol():String { + var attr:XML = data.@material[0]; + return (attr == null) ? null : attr.toString(); + } + + } +} + +import flash.geom.Point; + +class VertexChannelData { + public var values:Vector.; + public var stride:int; + public var offset:int; + public var index:int; + public var channel:Vector.; + public var inputSet:int; + + public function VertexChannelData(values:Vector., stride:int, offset:int, inputSet:int = 0) { + this.values = values; + this.stride = stride; + this.offset = offset; + this.inputSet = inputSet; + } + +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeSampler.as b/src/alternativa/engine3d/loaders/collada/DaeSampler.as new file mode 100644 index 0000000..e56fd38 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeSampler.as @@ -0,0 +1,118 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import alternativa.engine3d.animation.keys.NumberTrack; + import alternativa.engine3d.animation.keys.Track; + import alternativa.engine3d.animation.keys.TransformTrack; + + import flash.geom.Matrix3D; + + use namespace collada; + + /** + * @private + */ + public class DaeSampler extends DaeElement { + + private var times:Vector.; + private var values:Vector.; + private var timesStride:int; + private var valuesStride:int; + + public function DaeSampler(data:XML, document:DaeDocument) { + super(data, document); + } + + override protected function parseImplementation():Boolean { + var inputsList:XMLList = data.input; + + var inputSource:DaeSource; + var outputSource:DaeSource; + for (var i:int = 0, count:int = inputsList.length(); i < count; i++) { + var input:DaeInput = new DaeInput(inputsList[i], document); + var semantic:String = input.semantic; + if (semantic != null) { + switch (semantic) { + case "INPUT" : + inputSource = input.prepareSource(1); + if (inputSource != null) { + times = inputSource.numbers; + timesStride = inputSource.stride; + } + break; + case "OUTPUT" : + outputSource = input.prepareSource(1); + if (outputSource != null) { + values = outputSource.numbers; + valuesStride = outputSource.stride; + } + break; + } + } + } + return true; + } + + public function parseNumbersTrack(objectName:String, property:String):NumberTrack { + if (times != null && values != null && timesStride > 0) { + var track:NumberTrack = new NumberTrack(objectName, property); + var count:int = times.length/timesStride; + for (var i:int = 0; i < count; i++) { + track.addKey(times[int(timesStride*i)], values[int(valuesStride*i)]); + } + // TODO:: Exceptions with indices + return track; + } + return null; + } + + public function parseTransformationTrack(objectName:String):Track { + if (times != null && values != null && timesStride != 0) { + var track:TransformTrack = new TransformTrack(objectName); + var count:int = times.length/timesStride; + for (var i:int = 0; i < count; i++) { + var index:int = valuesStride*i; + var matrix:Matrix3D = new Matrix3D(Vector.([values[index], values[index + 4], values[index + 8], values[index + 12], + values[index + 1], values[index + 5], values[index + 9], values[index + 13], + values[index + 2], values[index + 6], values[index + 10], values[index + 14], + values[index + 3] ,values[index + 7], values[index + 11], values[index + 15]])); + track.addKey(times[i*timesStride], matrix); + } + return track; + } + return null; + } + + public function parsePointsTracks(objectName:String, xProperty:String, yProperty:String, zProperty:String):Vector. { + if (times != null && values != null && timesStride != 0) { + var xTrack:NumberTrack = new NumberTrack(objectName, xProperty); + xTrack.object = objectName; + var yTrack:NumberTrack = new NumberTrack(objectName, yProperty); + yTrack.object = objectName; + var zTrack:NumberTrack = new NumberTrack(objectName, zProperty); + zTrack.object = objectName; + var count:int = times.length/timesStride; + for (var i:int = 0; i < count; i++) { + var index:int = i*valuesStride; + var time:Number = times[i*timesStride]; + xTrack.addKey(time, values[index]); + yTrack.addKey(time, values[index + 1]); + zTrack.addKey(time, values[index + 2]); + } + return Vector.([xTrack, yTrack, zTrack]); + // TODO:: Exceptions with indices + } + return null; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeSource.as b/src/alternativa/engine3d/loaders/collada/DaeSource.as new file mode 100644 index 0000000..ee2e5ef --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeSource.as @@ -0,0 +1,164 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + /** + * @private + */ + public class DaeSource extends DaeElement { + + use namespace collada; + + /** + * Types of arrays. + */ + private const FLOAT_ARRAY:String = "float_array"; + private const INT_ARRAY:String = "int_array"; + private const NAME_ARRAY:String = "Name_array"; + + /** + * Array of Number items. + * Call parse() before using. + */ + public var numbers:Vector.; + /** + * Array of int items. + * Call parse() before using. + */ + public var ints:Vector.; + /** + * Array of string items. + * Call parse() before using. + */ + + public var names:Vector.; + /** + * Size in bytes of one number or int element + * Call parse() before using. + */ + public var stride:int; + + public function DaeSource(data:XML, document:DaeDocument) { + super(data, document); + + // array declares within in . + constructArrays(); + } + + private function constructArrays():void { + var children:XMLList = data.children(); + for (var i:int = 0, count:int = children.length(); i < count; i++) { + var child:XML = children[i]; + switch (child.localName()) { + case FLOAT_ARRAY : + case INT_ARRAY : + case NAME_ARRAY : + var array:DaeArray = new DaeArray(child, document); + if (array.id != null) { + document.arrays[array.id] = array; + } + break; + } + } + } + + private function get accessor():XML { + return data.technique_common.accessor[0]; + } + + override protected function parseImplementation():Boolean { + var accessor:XML = this.accessor; + if (accessor != null) { + var arrayXML:XML = accessor.@source[0]; + var array:DaeArray = (arrayXML == null) ? null : document.findArray(arrayXML); + if (array != null) { + var countXML:String = accessor.@count[0]; + if (countXML != null) { + var count:int = parseInt(countXML.toString(), 10); + var offsetXML:XML = accessor.@offset[0]; + var strideXML:XML = accessor.@stride[0]; + var offset:int = (offsetXML == null) ? 0 : parseInt(offsetXML.toString(), 10); + var stride:int = (strideXML == null) ? 1 : parseInt(strideXML.toString(), 10); + array.parse(); + if (array.array.length < (offset + (count*stride))) { + document.logger.logNotEnoughDataError(accessor); + return false; + } + this.stride = parseArray(offset, count, stride, array.array, array.type); + return true; + } + } else { + document.logger.logNotFoundError(arrayXML); + } + } + return false; + } + + private function numValidParams(params:XMLList):int { + var res:int = 0; + for (var i:int = 0, count:int = params.length(); i < count; i++) { + if (params[i].@name[0] != null) { + res++; + } + } + return res; + } + + private function parseArray(offset:int, count:int, stride:int, array:Array, type:String):int { + var params:XMLList = this.accessor.param; + var arrStride:int = Math.max(numValidParams(params), stride); + switch (type) { + case FLOAT_ARRAY: + numbers = new Vector.(int(arrStride*count)); + break; + case INT_ARRAY: + ints = new Vector.(int(arrStride*count)); + break; + case NAME_ARRAY: + names = new Vector.(int(arrStride*count)); + break; + } + var curr:int = 0; + for (var i:int = 0; i < arrStride; i++) { + // Only parameters which have name field should be read + var param:XML = params[i]; + if (param == null || param.hasOwnProperty("@name")) { + var j:int; + switch (type) { + case FLOAT_ARRAY: + for (j = 0; j < count; j++) { + var value:String = array[int(offset + stride*j + i)]; + if (value.indexOf(",") != -1) { + value = value.replace(/,/, "."); + } + numbers[int(arrStride*j + curr)] = parseFloat(value); + } + break; + case INT_ARRAY: + for (j = 0; j < count; j++) { + ints[int(arrStride*j + curr)] = parseInt(array[int(offset + stride*j + i)], 10); + } + break; + case NAME_ARRAY: + for (j = 0; j < count; j++) { + names[int(arrStride*j + curr)] = array[int(offset + stride*j + i)]; + } + break; + + } + curr++; + } + } + return arrStride; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeUnits.as b/src/alternativa/engine3d/loaders/collada/DaeUnits.as new file mode 100644 index 0000000..b5fee41 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeUnits.as @@ -0,0 +1,26 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + /** + * @private + */ + public class DaeUnits { + public static const METERS:Number = 1; + public static const DECIMETERS:Number = 0.1; + public static const CENTIMETERS:Number = 0.01; + public static const MILIMETERS:Number = 0.001; + public static const KILOMETERS:Number = 1000; + public static const INCHES:Number = 0.0254; + public static const FEET:Number = 0.3048; + public static const YARDS:Number = 0.9144; + public static const MILES:Number = 1609.347219; + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeVertex.as b/src/alternativa/engine3d/loaders/collada/DaeVertex.as new file mode 100644 index 0000000..5129b8b --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeVertex.as @@ -0,0 +1,76 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + import flash.geom.Vector3D; + + /** + * @private + */ + public class DaeVertex { + + public var vertexInIndex:int; + public var vertexOutIndex:int; + + public var indices:Vector. = new Vector.(); + + public var x:Number; + public var y:Number; + public var z:Number; + + public var uvs:Vector. = new Vector.(); + + public var normal:Vector3D; + public var tangent:Vector3D; + + public function addPosition(data:Vector., dataIndex:int, stride:int, unitScaleFactor:Number):void { + indices.push(dataIndex); + var offset:int = stride*dataIndex; + x = data[int(offset)]*unitScaleFactor; + y = data[int(offset + 1)]*unitScaleFactor; + z = data[int(offset + 2)]*unitScaleFactor; + } + + public function addNormal(data:Vector., dataIndex:int, stride:int):void { + indices.push(dataIndex); + var offset:int = stride*dataIndex; + normal = new Vector3D(); + normal.x = data[int(offset++)]; + normal.y = data[int(offset++)]; + normal.z = data[offset]; + } + + public function addTangentBiDirection(tangentData:Vector., tangentDataIndex:int, tangentStride:int, biNormalData:Vector., biNormalDataIndex:int, biNormalStride:int):void { + indices.push(tangentDataIndex); + indices.push(biNormalDataIndex); + var tangentOffset:int = tangentStride*tangentDataIndex; + var biNormalOffset:int = biNormalStride*biNormalDataIndex; + + var biNormalX:Number = biNormalData[int(biNormalOffset++)]; + var biNormalY:Number = biNormalData[int(biNormalOffset++)]; + var biNormalZ:Number = biNormalData[biNormalOffset]; + + tangent = new Vector3D(tangentData[int(tangentOffset++)], tangentData[int(tangentOffset++)], tangentData[tangentOffset]); + + var crossX:Number = normal.y*tangent.z - normal.z*tangent.y; + var crossY:Number = normal.z*tangent.x - normal.x*tangent.z; + var crossZ:Number = normal.x*tangent.y - normal.y*tangent.x; + var dot:Number = crossX*biNormalX + crossY*biNormalY + crossZ*biNormalZ; + tangent.w = dot < 0 ? -1 : 1; + } + + public function appendUV(data:Vector., dataIndex:int, stride:int):void { + indices.push(dataIndex); + uvs.push(data[int(dataIndex*stride)]); + uvs.push(1 - data[int(dataIndex*stride + 1)]); + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeVertexChannels.as b/src/alternativa/engine3d/loaders/collada/DaeVertexChannels.as new file mode 100644 index 0000000..8752f14 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeVertexChannels.as @@ -0,0 +1,29 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + + + /** + * @private + */ + public class DaeVertexChannels { + + public var uvChannels:Vector. = new Vector.(); + public var normalIndex:int; + public var tangentIndex:int; + public var biNormalIndex:int; + + public function DaeVertexChannels() { + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeVertices.as b/src/alternativa/engine3d/loaders/collada/DaeVertices.as new file mode 100644 index 0000000..d30bf37 --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeVertices.as @@ -0,0 +1,49 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + import alternativa.engine3d.alternativa3d; + + use namespace alternativa3d; + + /** + * @private + */ + public class DaeVertices extends DaeElement { + + use namespace collada; + + /** + * Source of vertex coordinates data. Stores coordinates in numbers array. + *stride property of source is not less than three. + * Call parse() before using. + */ + public var positions:DaeSource; + //private var texCoords:Vector.; + + public function DaeVertices(data:XML, document:DaeDocument) { + super(data, document); + } + + override protected function parseImplementation():Boolean { + // Get array of vertex coordinates. + var inputXML:XML = data.input.(@semantic == "POSITION")[0]; + if (inputXML != null) { + positions = (new DaeInput(inputXML, document)).prepareSource(3); + if (positions != null) { + return true; + } + } + return false; + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/DaeVisualScene.as b/src/alternativa/engine3d/loaders/collada/DaeVisualScene.as new file mode 100644 index 0000000..1b128be --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/DaeVisualScene.as @@ -0,0 +1,43 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + /** + * @private + */ + public class DaeVisualScene extends DaeElement { + + use namespace collada; + + public var nodes:Vector.; + + public function DaeVisualScene(data:XML, document:DaeDocument) { + super(data, document); + + // nodes are declared in . + constructNodes(); + } + + public function constructNodes():void { + var nodesList:XMLList = data.node; + var count:int = nodesList.length(); + nodes = new Vector.(count); + for (var i:int = 0; i < count; i++) { + var node:DaeNode = new DaeNode(nodesList[i], document, this); + if (node.id != null) { + document.nodes[node.id] = node; + } + nodes[i] = node; + } + } + + } +} diff --git a/src/alternativa/engine3d/loaders/collada/collada.as b/src/alternativa/engine3d/loaders/collada/collada.as new file mode 100644 index 0000000..82f845d --- /dev/null +++ b/src/alternativa/engine3d/loaders/collada/collada.as @@ -0,0 +1,17 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.collada { + + /** + * @private + */ + public namespace collada = "http://www.collada.org/2005/11/COLLADASchema"; +} diff --git a/src/alternativa/engine3d/loaders/events/TexturesLoaderEvent.as b/src/alternativa/engine3d/loaders/events/TexturesLoaderEvent.as new file mode 100644 index 0000000..51b7f88 --- /dev/null +++ b/src/alternativa/engine3d/loaders/events/TexturesLoaderEvent.as @@ -0,0 +1,69 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.loaders.events { + + import alternativa.engine3d.resources.ExternalTextureResource; + + import flash.display.BitmapData; + import flash.events.Event; + + /** + * The Event is dispatched when TexturesLoader complete loading of textures. + * + * @see alternativa.engine3d.loaders.TexturesLoader + */ + public class TexturesLoaderEvent extends Event { + /** + * Value for type property . + */ + public static const COMPLETE:String = "complete"; + + private var bitmapDatas:Vector.; + private var textures:Vector.; + + public function TexturesLoaderEvent(type:String, bitmapDatas:Vector., textures:Vector.) { + this.bitmapDatas = bitmapDatas; + this.textures = textures; + super(type, false, false); + } + + /** + * Returns the list of loaded images. Method returns the list only when method TexturesLoader.loadResource() is called + * and TexturesLoader.loadResources() with needBitmapData is set to true. + * + * @see alternativa.engine3d.loaders.TexturesLoader#loadResource() + * @see alternativa.engine3d.loaders.TexturesLoader#loadResources() + */ + public function getBitmapDatas():Vector. { + return bitmapDatas; + } + + /** + * Returns the list of loaded textures. Method returns the list only when method TexturesLoader.loadResource() is called + * and TexturesLoader.loadResources() with needBitmapData is set to true. + * + * @see alternativa.engine3d.loaders.TexturesLoader#loadResource() + * @see alternativa.engine3d.loaders.TexturesLoader#loadResources() + * @see alternativa.engine3d.resources.ExternalTextureResource + */ + public function getTextures():Vector. { + return textures; + } + + /** + * Returns copy of object. + */ + override public function clone():Event { + return new TexturesLoaderEvent(type, bitmapDatas, textures); + } + + } +} diff --git a/src/alternativa/engine3d/materials/A3DUtils.as b/src/alternativa/engine3d/materials/A3DUtils.as new file mode 100644 index 0000000..258e1f1 --- /dev/null +++ b/src/alternativa/engine3d/materials/A3DUtils.as @@ -0,0 +1,343 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.materials.compiler.CommandType; + import alternativa.engine3d.materials.compiler.VariableType; + + import avmplus.getQualifiedSuperclassName; + + import flash.display3D.Context3D; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.IndexBuffer3D; + import flash.display3D.VertexBuffer3D; + import flash.display3D.textures.Texture; + import flash.geom.Point; + import flash.utils.ByteArray; + import flash.utils.Dictionary; + import flash.utils.Endian; + import flash.utils.getDefinitionByName; + + /** + * @private + */ + public class A3DUtils { + + public static const NONE:int = 0; + public static const DXT1:int = 1; + public static const ETC1:int = 2; + public static const PVRTC:int = 3; + + private static const DXT1Data:ByteArray = getDXT1(); + private static const PVRTCData:ByteArray = getPVRTC(); + private static const ETC1Data:ByteArray = getETC1(); + + private static function getDXT1():ByteArray { + var DXT1Data:Vector. = Vector.([65,84,70,0,2,71,2,2,2,3,0,0,12,0,0,0,16,0,0,85,105,56,0,0,0,0,0,157,73,73,188,1,8,0,0,0,5,0,1,188,1,0,16,0,0,0,74,0,0,0,128,188,4,0,1,0,0,0,1,0,0,0,129,188,4,0,1,0,0,0,2,0,0,0,192,188,4,0,1,0,0,0,90,0,0,0,193,188,4,0,1,0,0,0,66,0,0,0,0,0,0,0,36,195,221,111,3,78,254,75,177,133,61,119,118,141,201,10,87,77,80,72,79,84,79,0,25,0,192,122,0,0,0,1,96,0,160,0,10,0,0,160,0,0,0,4,111,255,0,1,0,0,1,0,224,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,114,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,16,0,0,85,105,56,0,0,0,0,0,157,73,73,188,1,8,0,0,0,5,0,1,188,1,0,16,0,0,0,74,0,0,0,128,188,4,0,1,0,0,0,1,0,0,0,129,188,4,0,1,0,0,0,2,0,0,0,192,188,4,0,1,0,0,0,90,0,0,0,193,188,4,0,1,0,0,0,66,0,0,0,0,0,0,0,36,195,221,111,3,78,254,75,177,133,61,119,118,141,201,10,87,77,80,72,79,84,79,0,25,0,192,122,0,0,0,1,96,0,160,0,10,0,0,160,0,0,0,4,111,255,0,1,0,0,1,0,224,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,114,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,16,0,0,85,105,56,0,0,0,0,0,157,73,73,188,1,8,0,0,0,5,0,1,188,1,0,16,0,0,0,74,0,0,0,128,188,4,0,1,0,0,0,1,0,0,0,129,188,4,0,1,0,0,0,2,0,0,0,192,188,4,0,1,0,0,0,90,0,0,0,193,188,4,0,1,0,0,0,66,0,0,0,0,0,0,0,36,195,221,111,3,78,254,75,177,133,61,119,118,141,201,10,87,77,80,72,79,84,79,0,25,0,192,122,0,0,0,1,96,0,160,0,10,0,0,160,0,0,0,4,111,255,0,1,0,0,1,0,224,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,114,0,7,143,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + return getData(DXT1Data); + } + + private static function getETC1():ByteArray { + var ETC1Data:Vector. = Vector.([65,84,70,0,2,104,2,2,2,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,16,0,0,0,255,252,0,0,0,0,12,0,0,0,16,0,0,127,233,56,0,0,0,0,0,157,73,73,188,1,8,0,0,0,5,0,1,188,1,0,16,0,0,0,74,0,0,0,128,188,4,0,1,0,0,0,1,0,0,0,129,188,4,0,1,0,0,0,2,0,0,0,192,188,4,0,1,0,0,0,90,0,0,0,193,188,4,0,1,0,0,0,66,0,0,0,0,0,0,0,36,195,221,111,3,78,254,75,177,133,61,119,118,141,201,9,87,77,80,72,79,84,79,0,25,0,192,120,0,0,0,1,96,0,160,0,10,0,0,160,0,0,0,4,111,255,0,1,0,0,1,0,208,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,114,0,7,143,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,16,0,0,0,255,252,0,0,0,0,12,0,0,0,16,0,0,127,233,56,0,0,0,0,0,157,73,73,188,1,8,0,0,0,5,0,1,188,1,0,16,0,0,0,74,0,0,0,128,188,4,0,1,0,0,0,1,0,0,0,129,188,4,0,1,0,0,0,2,0,0,0,192,188,4,0,1,0,0,0,90,0,0,0,193,188,4,0,1,0,0,0,66,0,0,0,0,0,0,0,36,195,221,111,3,78,254,75,177,133,61,119,118,141,201,9,87,77,80,72,79,84,79,0,25,0,192,120,0,0,0,1,96,0,160,0,10,0,0,160,0,0,0,4,111,255,0,1,0,0,1,0,208,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,114,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,16,0,0,0,255,252,0,0,0,0,12,0,0,0,16,0,0,127,233,56,0,0,0,0,0,157,73,73,188,1,8,0,0,0,5,0,1,188,1,0,16,0,0,0,74,0,0,0,128,188,4,0,1,0,0,0,1,0,0,0,129,188,4,0,1,0,0,0,2,0,0,0,192,188,4,0,1,0,0,0,90,0,0,0,193,188,4,0,1,0,0,0,66,0,0,0,0,0,0,0,36,195,221,111,3,78,254,75,177,133,61,119,118,141,201,9,87,77,80,72,79,84,79,0,25,0,192,120,0,0,0,1,96,0,160,0,10,0,0,160,0,0,0,4,111,255,0,1,0,0,1,0,208,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,114,0,4,0]); + return getData(ETC1Data); + } + + private static function getPVRTC():ByteArray { + var PVRTCData:Vector. = Vector.([65,84,70,0,2,173,2,2,2,3,0,0,0,0,0,0,0,0,13,0,0,0,16,0,0,0,104,190,153,255,0,0,0,0,15,91,0,0,16,0,0,102,12,228,2,255,225,0,0,0,0,0,223,73,73,188,1,8,0,0,0,5,0,1,188,1,0,16,0,0,0,74,0,0,0,128,188,4,0,1,0,0,0,2,0,0,0,129,188,4,0,1,0,0,0,4,0,0,0,192,188,4,0,1,0,0,0,90,0,0,0,193,188,4,0,1,0,0,0,132,0,0,0,0,0,0,0,36,195,221,111,3,78,254,75,177,133,61,119,118,141,201,9,87,77,80,72,79,84,79,0,25,0,192,120,0,1,0,3,96,0,160,0,10,0,0,160,0,0,0,4,111,255,0,1,0,0,1,0,165,192,0,7,227,99,186,53,197,40,185,134,182,32,130,98,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,143,192,120,64,6,16,34,52,192,196,65,132,90,98,68,16,17,68,60,91,8,48,76,35,192,97,132,71,76,33,164,97,1,2,194,12,19,8,240,29,132,24,38,17,224,48,194,35,166,16,210,48,128,128,24,68,121,132,52,204,32,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,16,0,0,0,233,56,90,0,0,0,0,12,0,0,0,16,0,0,127,237,210,0,0,0,0,0,155,73,73,188,1,8,0,0,0,5,0,1,188,1,0,16,0,0,0,74,0,0,0,128,188,4,0,1,0,0,0,2,0,0,0,129,188,4,0,1,0,0,0,4,0,0,0,192,188,4,0,1,0,0,0,90,0,0,0,193,188,4,0,1,0,0,0,64,0,0,0,0,0,0,0,36,195,221,111,3,78,254,75,177,133,61,119,118,141,201,9,87,77,80,72,79,84,79,0,25,0,192,120,0,1,0,3,96,0,160,0,10,0,0,160,0,0,0,4,111,255,0,1,0,0,1,0,188,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,200,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,16,0,0,0,233,56,90,0,0,0,0,12,0,0,0,16,0,0,127,237,210,0,0,0,0,0,155,73,73,188,1,8,0,0,0,5,0,1,188,1,0,16,0,0,0,74,0,0,0,128,188,4,0,1,0,0,0,2,0,0,0,129,188,4,0,1,0,0,0,4,0,0,0,192,188,4,0,1,0,0,0,90,0,0,0,193,188,4,0,1,0,0,0,64,0,0,0,0,0,0,0,36,195,221,111,3,78,254,75,177,133,61,119,118,141,201,9,87,77,80,72,79,84,79,0,25,0,192,120,0,1,0,3,96,0,160,0,10,0,0,160,0,0,0,4,111,255,0,1,0,0,1,0,188,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,200,0,0,0,0,0,0,0,0,0,0]); + return getData(PVRTCData); + } + + private static function getData(source:Vector.):ByteArray { + var result:ByteArray = new ByteArray(); + for (var i:int = 0, length:int = source.length; i < length; i++) { + result.writeByte(source[i]); + } + return result; + } + + public static function getSizeFromATF(byteArray:ByteArray, size:Point):void { + byteArray.position = 7; + var w:int = byteArray.readByte(); + var h:int = byteArray.readByte(); + size.x = 1 << w; + size.y = 1 << h; + byteArray.position = 0; + } + + public static function getSupportedTextureFormat(context3D:Context3D):int { + var testTexture:Texture = context3D.createTexture(4, 4, Context3DTextureFormat.COMPRESSED, false); + var result:int = NONE; + try { + testTexture.uploadCompressedTextureFromByteArray(DXT1Data, 0); + result = DXT1; + } catch(e:Error) { + result = NONE; + } + if (result == NONE) { + try { + testTexture.uploadCompressedTextureFromByteArray(PVRTCData, 0); + result = PVRTC; + } catch(e:Error) { + result = NONE; + } + } + if (result == NONE) { + try { + testTexture.uploadCompressedTextureFromByteArray(ETC1Data, 0); + result = ETC1; + } catch(e:Error) { + result = NONE; + } + } + testTexture.dispose(); + return result; + } + + public static function vectorNumberToByteArray(vector:Vector.):ByteArray { + var result:ByteArray = new ByteArray(); + result.endian = Endian.LITTLE_ENDIAN; + for (var i:int = 0; i < vector.length; i++) { + result.writeFloat(vector[i]); + } + result.position = 0; + return result; + } + + public static function byteArrayToVectorUint(byteArray:ByteArray):Vector. { + var result:Vector. = new Vector.(); + var length:uint = 0; + byteArray.position = 0; + byteArray.endian = Endian.LITTLE_ENDIAN; + while (byteArray.bytesAvailable > 0) { + result[length++] = byteArray.readUnsignedShort(); + } + return result; + } + + public static function createVertexBufferFromByteArray(context:Context3D, byteArray:ByteArray, numVertices:uint, stride:uint = 3):VertexBuffer3D { + if (context == null) { + throw new ReferenceError("context is not set"); + } + var buffer:VertexBuffer3D = context.createVertexBuffer(numVertices, stride); + buffer.uploadFromByteArray(byteArray, 0, 0, numVertices); + return buffer; + } + + public static function createVertexBufferFromVector(context:Context3D, vector:Vector., numVertices:uint, stride:uint = 3):VertexBuffer3D { + if (context == null) { + throw new ReferenceError("context is not set"); + } + var buffer:VertexBuffer3D = context.createVertexBuffer(numVertices, stride); + + var byteArray:ByteArray = A3DUtils.vectorNumberToByteArray(vector); + buffer.uploadFromByteArray(byteArray, 0, 0, numVertices); + return buffer; + } + + public static function createTextureFromByteArray(context:Context3D, byteArray:ByteArray, width:Number, height:Number, format:String):Texture { + if (context == null) { + throw new ReferenceError("context is not set"); + } + var texture:Texture = context.createTexture(width, height, format, false); + texture.uploadCompressedTextureFromByteArray(byteArray, 0); + + return texture; + } + + public static function createIndexBufferFromByteArray(context:Context3D, byteArray:ByteArray, numIndices:uint):IndexBuffer3D { + if (context == null) { + throw new ReferenceError("context is not set"); + } + var buffer:IndexBuffer3D = context.createIndexBuffer(numIndices); + buffer.uploadFromByteArray(byteArray, 0, 0, numIndices); + + return buffer; + } + + public static function createIndexBufferFromVector(context:Context3D, vector:Vector., numIndices:int = -1):IndexBuffer3D { + if (context == null) { + throw new ReferenceError("context is not set"); + } + var count:uint = numIndices > 0 ? numIndices : vector.length; + var buffer:IndexBuffer3D = context.createIndexBuffer(count); + buffer.uploadFromVector(vector, 0, count); + + var byteArray:ByteArray = new ByteArray(); + byteArray.endian = Endian.LITTLE_ENDIAN; + for (var i:int = 0; i < count; i++) { + byteArray.writeInt(vector[i]); + } + byteArray.position = 0; + + buffer.uploadFromVector(vector, 0, count); + + return buffer; + } + + // Disassembler + private static var programType:Vector. = Vector.(["VERTEX", "FRAGMENT"]); + private static var samplerDimension:Vector. = Vector.(["2D", "cube", "3D"]); + private static var samplerWraping:Vector. = Vector.(["clamp", "repeat"]); + private static var samplerMipmap:Vector. = Vector.(["mipnone", "mipnearest", "miplinear"]); + private static var samplerFilter:Vector. = Vector.(["nearest", "linear"]); + private static var swizzleType:Vector. = Vector.(["x", "y", "z", "w"]); + private static var twoOperandsCommands:Dictionary; + private static const O_CODE:uint = "o".charCodeAt(0); + + public static function disassemble(byteCode:ByteArray):String { + if (!twoOperandsCommands) { + twoOperandsCommands = new Dictionary(); + twoOperandsCommands[0x1] = true; + twoOperandsCommands[0x2] = true; + twoOperandsCommands[0x3] = true; + twoOperandsCommands[0x4] = true; + twoOperandsCommands[0x6] = true; + twoOperandsCommands[0xb] = true; + twoOperandsCommands[0x11] = true; + twoOperandsCommands[0x12] = true; + twoOperandsCommands[0x13] = true; + twoOperandsCommands[0x17] = true; + twoOperandsCommands[0x18] = true; + twoOperandsCommands[0x19] = true; + twoOperandsCommands[0x28] = true; + twoOperandsCommands[0x29] = true; + twoOperandsCommands[0x2a] = true; + twoOperandsCommands[0x2c] = true; + twoOperandsCommands[0x2d] = true; + } + var res:String = ""; + byteCode.position = 0; + if (byteCode.bytesAvailable < 7) { + return "error in byteCode header"; + } + + res += "magic = " + byteCode.readUnsignedByte().toString(16); + res += "\nversion = " + byteCode.readInt().toString(10); + res += "\nshadertypeid = " + byteCode.readUnsignedByte().toString(16); + var pType:String = programType[byteCode.readByte()]; + res += "\nshadertype = " + pType; + res += "\nsource\n"; + pType = pType.substring(0, 1).toLowerCase(); + var lineNumber:uint = 1; + while (byteCode.bytesAvailable - 24 >= 0) { + res += (lineNumber++).toString() + ": " + getCommand(byteCode, pType) + "\n"; + } + if (byteCode.bytesAvailable > 0) { + res += "\nunexpected byteCode length. extra bytes:" + byteCode.bytesAvailable; + } + return res; + } + + private static function getCommand(byteCode:ByteArray, programType:String):String { + + var cmd:uint = byteCode.readUnsignedInt(); + var command:String = CommandType.COMMAND_NAMES[cmd]; + var result:String; + var destNumber:uint = byteCode.readUnsignedShort(); + var swizzle:uint = byteCode.readByte(); + var s:String = ""; + var destSwizzle:uint = 0; + if (swizzle < 15) { + s += "."; + s += ((swizzle & 0x1) > 0) ? "x" : ""; + s += ((swizzle & 0x2) > 0) ? "y" : ""; + s += ((swizzle & 0x4) > 0) ? "z" : ""; + s += ((swizzle & 0x8) > 0) ? "w" : ""; + destSwizzle = s.length; + } + + var destType:String = VariableType.TYPE_NAMES[byteCode.readUnsignedByte()].charAt(0); + if (destType.charCodeAt(0) == O_CODE) { + result = command + " " + attachProgramPrefix(destType, programType) + s + ", "; + } else { + result = command + " " + attachProgramPrefix(destType, programType) + destNumber.toString() + s + ", "; + } + + result += attachProgramPrefix(getSourceVariable(byteCode, destSwizzle), programType); + + if (twoOperandsCommands[cmd]) { + if (cmd == 0x28) { + result += ", " + attachProgramPrefix(getSamplerVariable(byteCode), programType); + } + else { + result += ", " + attachProgramPrefix(getSourceVariable(byteCode, destSwizzle), programType); + } + + } + else { + byteCode.readDouble(); + } + return result; + } + + private static function attachProgramPrefix(variable:String, programType:String):String { + var char:uint = variable.charCodeAt(0); + if (char == "o".charCodeAt(0)) + return variable + (programType == "f" ? "c" : "p"); + else if (char != "v".charCodeAt(0)) + return programType + variable; + return variable; + } + + private static function getSamplerVariable(byteCode:ByteArray):String { + var number:uint = byteCode.readUnsignedInt(); + byteCode.readByte(); + var dim:uint = byteCode.readByte() >> 4; + var wraping:uint = byteCode.readByte() >> 4; + var n:uint = byteCode.readByte(); + return "s" + number.toString() + " <" + samplerDimension[dim] + ", " + samplerWraping[wraping] + + ", " + samplerFilter[(n >> 4) & 0xf] + ", " + samplerMipmap[n & 0xf] + ">"; + } + + private static function getSourceVariable(byteCode:ByteArray, destSwizzle:uint):String { + var s1Number:uint = byteCode.readUnsignedShort(); + var offset:uint = byteCode.readUnsignedByte(); + var s:String = getSourceSwizzle(byteCode.readUnsignedByte(), destSwizzle); + + var s1Type:String = VariableType.TYPE_NAMES[byteCode.readUnsignedByte()].charAt(0); + var indexType:String = VariableType.TYPE_NAMES[byteCode.readUnsignedByte()].charAt(0); + var comp:String = swizzleType[byteCode.readUnsignedByte()]; + + if (byteCode.readUnsignedByte() > 0) { + return s1Type + "[" + indexType + s1Number.toString() + "." + comp + ((offset > 0) ? ("+" + offset.toString()) : "") + "]" + s; + } + return s1Type + s1Number.toString() + s; + } + + private static function getSourceSwizzle(swizzle:uint, destSwizzle:uint):String { + var s:String = ""; + if (swizzle != 0xe4) { + s += "."; + s += swizzleType[(swizzle & 0x3)]; + s += swizzleType[(swizzle >> 2) & 0x3]; + s += swizzleType[(swizzle >> 4) & 0x3]; + s += swizzleType[(swizzle >> 6) & 0x3]; + s = s.substring(0, destSwizzle > 0 ? destSwizzle : s.length); + } + return s; + } + + alternativa3d static function checkParent(child:Class, parent:Class):Boolean { + var current:Class = child; + if (parent == null) return true; + while (true) { + if (current == parent) return true; + var className:String = getQualifiedSuperclassName(current); + if (className != null) { + current = getDefinitionByName(className) as Class; + } else return false; + } + return false; + } + + } +} diff --git a/src/alternativa/engine3d/materials/EnvironmentMaterial.as b/src/alternativa/engine3d/materials/EnvironmentMaterial.as new file mode 100644 index 0000000..03fb4ae --- /dev/null +++ b/src/alternativa/engine3d/materials/EnvironmentMaterial.as @@ -0,0 +1,922 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.BitmapTextureResource; + import alternativa.engine3d.resources.Geometry; + import alternativa.engine3d.resources.TextureResource; + + import avmplus.getQualifiedClassName; + + import flash.display.BitmapData; + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DProgramType; + import flash.display3D.VertexBuffer3D; + import flash.display3D.textures.CubeTexture; + import flash.utils.Dictionary; + import flash.utils.getDefinitionByName; + + use namespace alternativa3d; + + /** + * The material which reflects the environment given with cube texture. + * + * @see alternativa.engine3d.resources.BitmapCubeTextureResource + * @see alternativa.engine3d.resources.ExternalTextureResource + */ + public class EnvironmentMaterial extends TextureMaterial { + + private static var caches:Dictionary = new Dictionary(true); + private var cachedContext3D:Context3D; + private var programsCache:Array; + + /** + * @private + */ + alternativa3d static var fogMode:int = FogMode.DISABLED; + /** + * @private + */ + alternativa3d static var fogNear:Number = 1000; + /** + * @private + */ + alternativa3d static var fogFar:Number = 5000; + + /** + * @private + */ + alternativa3d static var fogMaxDensity:Number = 1; + + /** + * @private + */ + alternativa3d static var fogColorR:Number = 0xC8/255; + /** + * @private + */ + alternativa3d static var fogColorG:Number = 0xA2/255; + /** + * @private + */ + alternativa3d static var fogColorB:Number = 0xC8/255; + + /** + * @private + */ + alternativa3d static var fogTexture:TextureResource; + + /** + * @private + */ + static alternativa3d const _passReflectionProcedure:Procedure = new Procedure([ + // i0 = position, i1 = normal + "#v1=vNormal", + "#v0=vPosition", + "mov v0, i0", + "mov v1, i1" + ], "passReflectionProcedure"); + + /** + * @private + */ + static alternativa3d const _applyReflectionProcedure:Procedure = getApplyReflectionProcedure(); + + private static function getApplyReflectionProcedure():Procedure { + var result:Procedure = new Procedure([ + "#v1=vNormal", + "#v0=vPosition", + "#s0=sCubeMap", + "#c0=cCamera", + "sub t0, v0, c0", + "dp3 t1.x, v1, t0", + "add t1.x, t1.x, t1.x", + "mul t1, v1, t1.x", + "sub t1, t0, t1", + "nrm t1.xyz, t1.xyz", + "m33 t1.xyz, t1.xyz, c1", + "nrm t1.xyz, t1.xyz", + "tex o0, t1, s0 " + ], "applyReflectionProcedure"); + result.assignVariableName(VariableType.CONSTANT, 1, "cLocalToGlobal", 3); + return result; + } + + /** + * @private + */ + static alternativa3d const _applyReflectionNormalMapProcedure:Procedure = getApplyReflectionNormalMapProcedure(); + + private static function getApplyReflectionNormalMapProcedure():Procedure { + var result:Procedure = new Procedure([ + "#s0=sCubeMap", + "#c0=cCamera", + "#v0=vPosition", + "sub t0, v0, c0", + "dp3 t1.x, i0.xyz, t0", + "add t1.x, t1.x, t1.x", + "mul t1, i0.xyz, t1.x", + "sub t1, t0, t1", + "nrm t1.xyz, t1.xyz", + "m33 t1.xyz, t1.xyz, c1", + "nrm t1.xyz, t1.xyz", + "tex o0, t1, s0 " + ], "applyReflectionNormalMapProcedure"); + result.assignVariableName(VariableType.CONSTANT, 1, "cLocalToGlobal", 3); + return result; + } + + /** + * @private + */ + static alternativa3d const _blendReflection:Procedure = new Procedure([ + "#c0=cAlpha", + "mul t1.xyz, i0.xyz, c0.y", + "mul t0.xyz, i1, c0.z", + "add t0.xyz, t1.xyz, t0", + "mov t0.w, i0.w", + "mov o0, t0" + ], "blendReflection"); + + /** + * @private + */ + static alternativa3d const _blendReflectionMap:Procedure = new Procedure([ + "#c0=cCamera", + "#c1=cAlpha", + "#s0=sReflection", + "#v0=vUV", + "tex t0, v0, s0 <2d,repeat,linear,miplinear>", + "mul t0, t0, c1.z", + "mul t1.xyz, i1, t0", + "sub t0, c0.www, t0", + "mul t2, i0, t0", + "add t0.xyz, t1, t2", + "mov t0.w, i0.w", + "mov o0, t0" + ], "blendReflectionMap"); + + // inputs : tangent, normal + private static const _passTBNRightProcedure:Procedure = getPassTBNProcedure(true); + private static const _passTBNLeftProcedure:Procedure = getPassTBNProcedure(false); + + private static function getPassTBNProcedure(right:Boolean):Procedure { + var crsInSpace:String = (right) ? "crs t1.xyz, i0, i1" : "crs t1.xyz, i1, i0"; + return new Procedure([ + "#v0=vTangent", + "#v1=vBinormal", + "#v2=vNormal", + // Calculates binormal + crsInSpace, + "mul t1.xyz, t1.xyz, i0.w", + // Transpose normal matrix + "mov v0.x, i0.x", + "mov v0.y, t1.x", + "mov v0.z, i1.x", + "mov v0.w, i1.w", + "mov v1.x, i0.y", + "mov v1.y, t1.y", + "mov v1.z, i1.y", + "mov v1.w, i1.w", + "mov v2.x, i0.z", + "mov v2.y, t1.z", + "mov v2.z, i1.z", + "mov v2.w, i1.w" + ], "passTBNProcedure"); + } + + // outputs : normal, viewVector + private static const _getNormalTangentProcedure:Procedure = new Procedure([ + "#v0=vTangent", + "#v1=vBinormal", + "#v2=vNormal", + "#v3=vUV", + "#c0=cCamera", + "#s0=sBump", + // Extract normal from the texture + "tex t0, v3, s0 <2d,repeat,linear,miplinear>", + "add t0, t0, t0", + "sub t0.xyz, t0.xyz, c0.www", + // Transform the normal with TBN + "nrm t1.xyz, v0.xyz", + "dp3 o0.x, t0.xyz, t1.xyz", + "nrm t1.xyz, v1.xyz", + "dp3 o0.y, t0.xyz, t1.xyz", + "nrm t1.xyz, v2.xyz", + "dp3 o0.z, t0.xyz, t1.xyz", + // Normalization after transform + "nrm o0.xyz, o0.xyz" + ], "getNormalTangentProcedure"); + // outputs : normal, viewVector + private static const _getNormalObjectProcedure:Procedure = new Procedure([ + "#v3=vUV", + "#c0=cCamera", + "#s0=sBump", + // Extract normal from the texture + "tex t0, v3, s0 <2d,repeat,linear,miplinear>", + "add t0, t0, t0", + "sub t0.xyz, t0.xyz, c0.www", + // Normalization + "nrm o0.xyz, t0.xyz" + ], "getNormalObjectProcedure"); + + // inputs : position + private static const passSimpleFogConstProcedure:Procedure = new Procedure([ + "#v0=vZDistance", + "#c0=cFogSpace", + "dp4 t0.z, i0, c0", + "mov v0, t0.zzzz", + "sub v0.y, i0.w, t0.z" + ], "passSimpleFogConst"); + + // inputs : color + private static const outputWithSimpleFogProcedure:Procedure = new Procedure([ + "#v0=vZDistance", + "#c0=cFogColor", + "#c1=cFogRange", + // Restrict fog factor with the range + "min t0.xy, v0.xy, c1.xy", + "max t0.xy, t0.xy, c1.zw", + "mul i0.xyz, i0.xyz, t0.y", + "mul t0.xyz, c0.xyz, t0.x", + "add i0.xyz, i0.xyz, t0.xyz", + "mov o0, i0" + ], "outputWithSimpleFog"); + + // inputs : position, projected + private static const postPassAdvancedFogConstProcedure:Procedure = new Procedure([ + "#v0=vZDistance", + "#c0=cFogSpace", + "dp4 t0.z, i0, c0", + "mov v0, t0.zzzz", + "sub v0.y, i0.w, t0.z", + // Screen x coordinate + "mov v0.zw, i1.xwxw", + "mov o0, i1" + ], "postPassAdvancedFogConst"); + + // inputs : color + private static const outputWithAdvancedFogProcedure:Procedure = new Procedure([ + "#v0=vZDistance", + "#c0=cFogConsts", + "#c1=cFogRange", + "#s0=sFogTexture", + // Restrict fog factor with the range + "min t0.xy, v0.xy, c1.xy", + "max t0.xy, t0.xy, c1.zw", + "mul i0.xyz, i0.xyz, t0.y", + // Calculate fog color + "mov t1.xyzw, c0.yyzw", + "div t0.z, v0.z, v0.w", + "mul t0.z, t0.z, c0.x", + "add t1.x, t1.x, t0.z", + "tex t1, t1, s0 <2d,repeat,linear,miplinear>", + "mul t0.xyz, t1.xyz, t0.x", + "add i0.xyz, i0.xyz, t0.xyz", + "mov o0, i0" + ], "outputWithAdvancedFog"); + + private static const _applyLightMapProcedure:Procedure = new Procedure([ + "#v0=vUV1", + "#s0=sLightMap", + "tex t0, v0, s0 <2d,repeat,linear,miplinear>", + "add t0, t0, t0", + "mul o0.xyz, i0.xyz, t0.xyz" + ], "applyLightMapProcedure"); + + private static const _passLightMapUVProcedure:Procedure = new Procedure([ + "#a0=aUV1", + "#v0=vUV1", + "mov v0, a0" + ], "passLightMapUVProcedure"); + + private var _normalMapSpace:int = NormalMapSpace.TANGENT_RIGHT_HANDED; + + /** + * Type of the normal map. Should be defined by constants of NormalMapSpace class. + * @default NormalMapSpace.TANGENT + * + * @see NormalMapSpace + */ + public function get normalMapSpace():int { + return _normalMapSpace; + } + + /** + * @private + */ + public function set normalMapSpace(value:int):void { + if (value != NormalMapSpace.TANGENT_RIGHT_HANDED && value != NormalMapSpace.TANGENT_LEFT_HANDED && value != NormalMapSpace.OBJECT) { + throw new ArgumentError("Value must be a constant from the NormalMapSpace class"); + } + + _normalMapSpace = value; + dirty(); + } + + /** + * Normal map. + */ + public function get normalMap():TextureResource { + return _normalMap; + } + + /** + * @private + */ + public function set normalMap(value:TextureResource):void { + _normalMap = value; + dirty(); + } + + /** + * Reflection texture. Should be BitmapCubeTextureResource or ExternalTextureResource with CubeTexture data. + */ + public function get environmentMap():TextureResource { + return _environmentMap; + } + + /** + * @private + */ + public function set environmentMap(value:TextureResource):void { + _environmentMap = value; + dirty(); + } + + /** + * Reflectivity map. + */ + public function get reflectionMap():TextureResource { + return _reflectionMap; + } + + /** + * @private + */ + public function set reflectionMap(value:TextureResource):void { + _reflectionMap = value; + dirty(); + } + + /** + * Light map. + */ + public function get lightMap():TextureResource { + return _lightMap; + } + + /** + * @private + */ + public function set lightMap(value:TextureResource):void { + _lightMap = value; + dirty(); + } + + /** + * Reflectivity. + */ + public var reflection:Number = 1; + + /** + * Number of the UV-channel for light map. + */ + public var lightMapChannel:uint = 1; + + /** + * @private + */ + alternativa3d var _normalMap:TextureResource; + + /** + * @private + */ + alternativa3d var _environmentMap:TextureResource; + + /** + * @private + */ + alternativa3d var _reflectionMap:TextureResource; + + /** + * @private + */ + alternativa3d var _lightMap:TextureResource; + + private var localToGlobalTransform:Transform3D = new Transform3D(); + + /*alternativa3d var lightMapOptions:SamplerOptions = new SamplerOptions(this); + + alternativa3d var normalMapOptions:SamplerOptions = new SamplerOptions(this); + + alternativa3d var environmentMapOptions:SamplerOptions = new SamplerOptions(this); + + alternativa3d var reflectionMapOptions:SamplerOptions = new SamplerOptions(this); + + alternativa3d var diffuseMapOptions:SamplerOptions = new SamplerOptions(this);*/ + + /** + * Creates a new EnvironmentMaterial instance. + * @param diffuseMap + * @param environmentMap + * @param normalMap + * @param reflectionMap + * @param lightMap + * @param opacityMap + * @param alpha + */ + public function EnvironmentMaterial(diffuseMap:TextureResource = null, environmentMap:TextureResource = null, normalMap:TextureResource = null, reflectionMap:TextureResource = null, lightMap:TextureResource = null, opacityMap:TextureResource = null, alpha:Number = 1) { + super(diffuseMap, opacityMap, alpha); + this._environmentMap = environmentMap; + this._normalMap = normalMap; + this._reflectionMap = reflectionMap; + this._lightMap = lightMap; + } + + /** + * @inheritDoc + */ + override public function clone():Material { + var res:EnvironmentMaterial = new EnvironmentMaterial(diffuseMap, _environmentMap, _normalMap, _reflectionMap, _lightMap, opacityMap, alpha); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Material):void { + super.clonePropertiesFrom(source); + var eMaterial:EnvironmentMaterial = EnvironmentMaterial(source); + reflection = eMaterial.reflection; + lightMapChannel = eMaterial.lightMapChannel; + _normalMapSpace = eMaterial._normalMapSpace; + } + + /** + * @private + */ + alternativa3d override function fillResources(resources:Dictionary, resourceType:Class):void { + super.alternativa3d::fillResources(resources, resourceType); + if (_environmentMap != null && A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(_environmentMap)) as Class, resourceType)) { + resources[_environmentMap] = true; + } + if (_normalMap != null && A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(_normalMap)) as Class, resourceType)) { + resources[_normalMap] = true; + } + if (_reflectionMap != null && A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(_reflectionMap)) as Class, resourceType)) { + resources[_reflectionMap] = true; + } + if (_lightMap != null && A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(_lightMap)) as Class, resourceType)) { + resources[_lightMap] = true; + } + } + + private function setupProgram(targetObject:Object3D, opacityMap:TextureResource, alphaTest:int):EnvironmentMaterialShaderProgram { + var vertexLinker:Linker = new Linker(Context3DProgramType.VERTEX); + var fragmentLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + var positionVar:String = "aPosition"; + var normalVar:String = "aNormal"; + var tangentVar:String = "aTangent"; + vertexLinker.declareVariable(positionVar, VariableType.ATTRIBUTE); + vertexLinker.declareVariable(normalVar, VariableType.ATTRIBUTE); + if (targetObject.transformProcedure != null) { + positionVar = appendPositionTransformProcedure(targetObject.transformProcedure, vertexLinker); + } + var procedure:Procedure; + if (targetObject.deltaTransformProcedure != null) { + vertexLinker.declareVariable("tTransformedNormal"); + procedure = targetObject.deltaTransformProcedure.newInstance(); + vertexLinker.addProcedure(procedure); + vertexLinker.setInputParams(procedure, normalVar); + vertexLinker.setOutputParams(procedure, "tTransformedNormal"); + normalVar = "tTransformedNormal"; + + if ((_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED || _normalMapSpace == NormalMapSpace.TANGENT_LEFT_HANDED) && _normalMap != null) { + vertexLinker.declareVariable(tangentVar, VariableType.ATTRIBUTE); + vertexLinker.declareVariable("tTransformedTangent"); + procedure = targetObject.deltaTransformProcedure.newInstance(); + vertexLinker.addProcedure(procedure); + vertexLinker.setInputParams(procedure, tangentVar); + vertexLinker.setOutputParams(procedure, "tTransformedTangent"); + tangentVar = "tTransformedTangent"; + } + + } else { + if ((_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED || _normalMapSpace == NormalMapSpace.TANGENT_LEFT_HANDED) && _normalMap != null) { + vertexLinker.declareVariable(tangentVar, VariableType.ATTRIBUTE); + } + } + if (_lightMap != null) { + vertexLinker.addProcedure(_passLightMapUVProcedure); + } + + vertexLinker.addProcedure(_passReflectionProcedure); + vertexLinker.setInputParams(_passReflectionProcedure, positionVar, normalVar); + vertexLinker.addProcedure(_projectProcedure); + vertexLinker.setInputParams(_projectProcedure, positionVar); + vertexLinker.addProcedure(_passUVProcedure); + if (_normalMap != null) { + fragmentLinker.declareVariable("tNormal"); + if (_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED || _normalMapSpace == NormalMapSpace.TANGENT_LEFT_HANDED) { + var nrmProcedure:Procedure = (_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED) ? _passTBNRightProcedure : _passTBNLeftProcedure; + vertexLinker.addProcedure(nrmProcedure); + vertexLinker.setInputParams(nrmProcedure, tangentVar, normalVar); + fragmentLinker.addProcedure(_getNormalTangentProcedure); + fragmentLinker.setOutputParams(_getNormalTangentProcedure, "tNormal"); + } else { + fragmentLinker.addProcedure(_getNormalObjectProcedure); + fragmentLinker.setOutputParams(_getNormalObjectProcedure, "tNormal"); + } + } + + fragmentLinker.declareVariable("tColor"); + outputProcedure = opacityMap != null ? getDiffuseOpacityProcedure : getDiffuseProcedure; + fragmentLinker.addProcedure(outputProcedure); + fragmentLinker.setOutputParams(outputProcedure, "tColor"); + + if (alphaTest > 0) { + outputProcedure = alphaTest == 1 ? thresholdOpaqueAlphaProcedure : thresholdTransparentAlphaProcedure; + fragmentLinker.addProcedure(outputProcedure, "tColor"); + fragmentLinker.setOutputParams(outputProcedure, "tColor"); + } + + fragmentLinker.declareVariable("tReflection"); + if (_normalMap != null) { + fragmentLinker.addProcedure(_applyReflectionNormalMapProcedure); + fragmentLinker.setInputParams(_applyReflectionNormalMapProcedure, "tNormal"); + fragmentLinker.setOutputParams(_applyReflectionNormalMapProcedure, "tReflection"); + } else { + fragmentLinker.addProcedure(_applyReflectionProcedure); + fragmentLinker.setOutputParams(_applyReflectionProcedure, "tReflection"); + } + if (_lightMap != null) { + fragmentLinker.addProcedure(_applyLightMapProcedure); + fragmentLinker.setInputParams(_applyLightMapProcedure, "tColor"); + fragmentLinker.setOutputParams(_applyLightMapProcedure, "tColor"); + } + + var outputProcedure:Procedure; + if (_reflectionMap != null) { + fragmentLinker.addProcedure(_blendReflectionMap); + fragmentLinker.setInputParams(_blendReflectionMap, "tColor", "tReflection"); + outputProcedure = _blendReflectionMap; + } else { + fragmentLinker.addProcedure(_blendReflection); + fragmentLinker.setInputParams(_blendReflection, "tColor", "tReflection"); + outputProcedure = _blendReflection; + } + + if (fogMode == FogMode.SIMPLE || fogMode == FogMode.ADVANCED) { + fragmentLinker.setOutputParams(outputProcedure, "tColor"); + } + if (fogMode == FogMode.SIMPLE) { + vertexLinker.addProcedure(passSimpleFogConstProcedure); + vertexLinker.setInputParams(passSimpleFogConstProcedure, positionVar); + fragmentLinker.addProcedure(outputWithSimpleFogProcedure); + fragmentLinker.setInputParams(outputWithSimpleFogProcedure, "tColor"); + } else if (fogMode == FogMode.ADVANCED) { + vertexLinker.declareVariable("tProjected"); + vertexLinker.setOutputParams(_projectProcedure, "tProjected"); + vertexLinker.addProcedure(postPassAdvancedFogConstProcedure); + vertexLinker.setInputParams(postPassAdvancedFogConstProcedure, positionVar, "tProjected"); + fragmentLinker.addProcedure(outputWithAdvancedFogProcedure); + fragmentLinker.setInputParams(outputWithAdvancedFogProcedure, "tColor"); + } + + fragmentLinker.varyings = vertexLinker.varyings; + return new EnvironmentMaterialShaderProgram(vertexLinker, fragmentLinker); + } + + /** + * @private + */ + alternativa3d function getProceduresCRC32(targetObject:Object3D, opacityMap:TextureResource, alphaTest:int):uint { + var crc:uint = 0xFFFFFFFF; + var procedureCRC:uint; + var crc32Table:Vector. = Procedure.crc32Table; + if (targetObject.transformProcedure != null) { + procedureCRC = targetObject.transformProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } + if (targetObject.deltaTransformProcedure != null) { + procedureCRC = targetObject.deltaTransformProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + if ((_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED || _normalMapSpace == NormalMapSpace.TANGENT_LEFT_HANDED) && _normalMap != null) { + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } + + } + if (_lightMap != null) { + procedureCRC = _passLightMapUVProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } + if (_normalMap != null) { + if (_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED || _normalMapSpace == NormalMapSpace.TANGENT_LEFT_HANDED) { + procedureCRC = (_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED) ? _passTBNRightProcedure.crc32 : _passTBNLeftProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + + procedureCRC = _getNormalTangentProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } else { + procedureCRC = _getNormalObjectProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } + } + procedureCRC = opacityMap != null ? getDiffuseOpacityProcedure.crc32 : getDiffuseProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + if (alphaTest > 0) { + procedureCRC = alphaTest == 1 ? thresholdOpaqueAlphaProcedure.crc32 : thresholdTransparentAlphaProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } + if (_normalMap != null) { + procedureCRC = _applyReflectionNormalMapProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } else { + procedureCRC = _applyReflectionProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } + if (_lightMap != null) { + procedureCRC = _applyLightMapProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } + if (_reflectionMap != null) { + procedureCRC = _blendReflectionMap.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } else { + procedureCRC = _blendReflection.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } + if (fogMode == FogMode.SIMPLE) { + procedureCRC = passSimpleFogConstProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + procedureCRC = outputWithSimpleFogProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } else if (fogMode == FogMode.ADVANCED) { + procedureCRC = postPassAdvancedFogConstProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + procedureCRC = outputWithAdvancedFogProcedure.crc32; + crc = crc32Table[(crc ^ procedureCRC) & 0xFF] ^ (crc >> 8); + } + return crc ^ 0xFFFFFFFF; + } + + private function getDrawUnit(program:EnvironmentMaterialShaderProgram, camera:Camera3D, surface:Surface, geometry:Geometry, opacityMap:TextureResource):DrawUnit { + // Buffers + var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]); + var normalsBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.NORMAL); + var tangentsBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TANGENT4); + if (positionBuffer == null || uvBuffer == null || normalsBuffer == null) return null; + var i:int; + var object:Object3D = surface.object; + + if (program.sBump >= 0 && (_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED || _normalMapSpace == NormalMapSpace.TANGENT_LEFT_HANDED)) { + if (tangentsBuffer == null) return null; + } + + // Draw call + var drawUnit:DrawUnit = camera.renderer.createDrawUnit(object, program.program, geometry._indexBuffer, surface.indexBegin, surface.numTriangles, program); + + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cThresholdAlpha"), alphaThreshold, 0, 0, alpha); + // Set the textures + if (program.sLightMap >= 0) { + drawUnit.setTextureAt(program.sLightMap, _lightMap._texture); + drawUnit.setVertexBufferAt(program.aUV1, uvBuffer, geometry._attributesOffsets[VertexAttributes.TEXCOORDS[lightMapChannel]], VertexAttributes.FORMATS[VertexAttributes.TEXCOORDS[lightMapChannel]]); + } + + if (program.sBump >= 0) { + drawUnit.setTextureAt(program.sBump, _normalMap._texture); + if (_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED || _normalMapSpace == NormalMapSpace.TANGENT_LEFT_HANDED) { + drawUnit.setVertexBufferAt(program.aTangent, tangentsBuffer, geometry._attributesOffsets[VertexAttributes.TANGENT4], VertexAttributes.FORMATS[VertexAttributes.TANGENT4]); + } + } + + if (program.sReflection >= 0) { + drawUnit.setTextureAt(program.sReflection, _reflectionMap._texture); + } + + if (program.sOpacity >= 0) { + drawUnit.setTextureAt(program.sOpacity, opacityMap._texture); + } + + // Set the streams + drawUnit.setVertexBufferAt(program.aPosition, positionBuffer, geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + drawUnit.setVertexBufferAt(program.aUV, uvBuffer, geometry._attributesOffsets[VertexAttributes.TEXCOORDS[0]], VertexAttributes.FORMATS[VertexAttributes.TEXCOORDS[0]]); + drawUnit.setVertexBufferAt(program.aNormal, normalsBuffer, geometry._attributesOffsets[VertexAttributes.NORMAL], VertexAttributes.FORMATS[VertexAttributes.NORMAL]); + + // Set the constants + object.setTransformConstants(drawUnit, surface, program.vertexShader, camera); + drawUnit.setProjectionConstants(camera, program.cProjMatrix, object.localToCameraTransform); + + drawUnit.setTextureAt(program.sTexture, diffuseMap._texture); + drawUnit.setTextureAt(program.sCubeMap, _environmentMap._texture); + var cameraToLocalTransform:Transform3D = object.cameraToLocalTransform; + drawUnit.setFragmentConstantsFromNumbers(program.cCamera, cameraToLocalTransform.d, cameraToLocalTransform.h, cameraToLocalTransform.l); + drawUnit.setFragmentConstantsFromNumbers(program.cAlpha, 0, 1 - reflection, reflection, alpha); + + // Calculate local to global matrix + localToGlobalTransform.combine(camera.localToGlobalTransform, object.localToCameraTransform); + drawUnit.setFragmentConstantsFromTransform(program.cLocalToGlobal, localToGlobalTransform); + if (fogMode == FogMode.SIMPLE || fogMode == FogMode.ADVANCED) { + var lm:Transform3D = object.localToCameraTransform; + var dist:Number = fogFar - fogNear; + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("cFogSpace"), lm.i/dist, lm.j/dist, lm.k/dist, (lm.l - fogNear)/dist); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cFogRange"), fogMaxDensity, 1, 0, 1 - fogMaxDensity); + } + if (fogMode == FogMode.SIMPLE) { + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cFogColor"), fogColorR, fogColorG, fogColorB); + } + if (fogMode == FogMode.ADVANCED) { + if (fogTexture == null) { + var bmd:BitmapData = new BitmapData(32, 1, false, 0xFF0000); + for (i = 0; i < 32; i++) { + bmd.setPixel(i, 0, ((i/32)*255) << 16); + } + fogTexture = new BitmapTextureResource(bmd); + fogTexture.upload(camera.context3D); + } + var cLocal:Transform3D = camera.localToGlobalTransform; + var halfW:Number = camera.view.width/2; + var leftX:Number = -halfW*cLocal.a + camera.focalLength*cLocal.c; + var leftY:Number = -halfW*cLocal.e + camera.focalLength*cLocal.g; + var rightX:Number = halfW*cLocal.a + camera.focalLength*cLocal.c; + var rightY:Number = halfW*cLocal.e + camera.focalLength*cLocal.g; + // UV + var angle:Number = (Math.atan2(leftY, leftX) - Math.PI/2); + if (angle < 0) angle += Math.PI*2; + var dx:Number = rightX - leftX; + var dy:Number = rightY - leftY; + var lens:Number = Math.sqrt(dx*dx + dy*dy); + leftX /= lens; + leftY /= lens; + rightX /= lens; + rightY /= lens; + var uScale:Number = Math.acos(leftX*rightX + leftY*rightY)/Math.PI/2; + var uRight:Number = angle/Math.PI/2; + + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cFogConsts"), 0.5*uScale, 0.5 - uRight, 0); + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sFogTexture"), fogTexture._texture); + } + return drawUnit; + } + + private function getProgram(targetObject:Object3D, camera:Camera3D, opacityMap:TextureResource, alphaTest:int):EnvironmentMaterialShaderProgram { + // Renew program cache for this context + if (camera.context3D != cachedContext3D) { + cachedContext3D = camera.context3D; + programsCache = caches[cachedContext3D]; + if (programsCache == null) { + programsCache = new Array(); + caches[cachedContext3D] = programsCache; + } + } + + var key:uint; + var program:EnvironmentMaterialShaderProgram; + key = getProceduresCRC32(targetObject, opacityMap, alphaTest); + program = programsCache[key]; + if (program == null) { + program = programsCache[key] = setupProgram(targetObject, opacityMap, alphaTest); + + program.upload(camera.context3D); + } + return program; + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, objectRenderPriority:int = -1):void { + if (diffuseMap == null || diffuseMap._texture == null) return; + if (_environmentMap == null || _environmentMap._texture == null || !(_environmentMap._texture is CubeTexture)) return; + if (opacityMap != null && opacityMap._texture == null) return; + if (_normalMap != null && _normalMap._texture == null) return; + if (_reflectionMap != null && _reflectionMap._texture == null) return; + if (_lightMap != null && _lightMap._texture == null) return; + var object:Object3D = surface.object; + + // Program + var program:EnvironmentMaterialShaderProgram; + var drawUnit:DrawUnit; + // Opaque pass + if (opaquePass && alphaThreshold <= alpha) { + if (alphaThreshold > 0) { + // Alpha test + // use opacityMap if it is presented + program = getProgram(object, camera, opacityMap, 1); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap); + } else { + // do not use opacityMap at all + program = getProgram(object, camera, null, 0); + drawUnit = getDrawUnit(program, camera, surface, geometry, null); + } + if (drawUnit == null) return; + // Use z-buffer within DrawCall, draws without blending + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.OPAQUE); + } + // Transparent pass + if (transparentPass && alphaThreshold > 0 && alpha > 0) { + // use opacityMap if it is presented + if (alphaThreshold <= alpha && !opaquePass) { + // Alpha threshold + program = getProgram(object, camera, opacityMap, 2); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap); + } else { + // There is no Alpha threshold or check z-buffer by previous pass + program = getProgram(object, camera, opacityMap, 0); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap); + } + if (drawUnit == null) return; + // Do not use z-buffer, draws with blending + drawUnit.blendSource = Context3DBlendFactor.SOURCE_ALPHA; + drawUnit.blendDestination = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA; + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.TRANSPARENT_SORT); + } + } + +// + /** + * @private + */ + alternativa3d function dirty():void { + for each (var program:EnvironmentMaterialShaderProgram in programsCache) { + program.dirty = true; + } + } + + } +} + +import alternativa.engine3d.alternativa3d; +import alternativa.engine3d.materials.ShaderProgram; +import alternativa.engine3d.materials.compiler.Linker; + +use namespace alternativa3d; + +class EnvironmentMaterialShaderProgram extends ShaderProgram { + + public var aTangent:int = -1; + public var aNormal:int = -1; + public var aPosition:int = -1; + public var aUV:int = -1; + public var aUV1:int = -1; + + public var cCamera:int = -1; + public var cLocalToGlobal:int = -1; + public var cAlpha:int = -1; + public var cProjMatrix:int = -1; + + public var sBump:int = -1; + public var sTexture:int = -1; + public var sOpacity:int = -1; + public var sCubeMap:int = -1; + public var sReflection:int = -1; + public var sLightMap:int = -1; + public var dirty:Boolean = false; + + public function EnvironmentMaterialShaderProgram(vertexShader:Linker, fragmentShader:Linker) { + super(vertexShader, fragmentShader); + fragmentShader.varyings = vertexShader.varyings; + vertexShader.link(); + fragmentShader.link(); + aPosition = vertexShader.findVariable("aPosition"); + aNormal = vertexShader.findVariable("aNormal"); + aUV = vertexShader.findVariable("aUV"); + sBump = fragmentShader.findVariable("sBump"); + aTangent = vertexShader.findVariable("aTangent"); + sReflection = fragmentShader.findVariable("sReflection"); + sLightMap = fragmentShader.findVariable("sLightMap"); + aUV1 = vertexShader.findVariable("aUV1"); + cProjMatrix = vertexShader.findVariable("cProjMatrix"); + sTexture = fragmentShader.findVariable("sDiffuse"); + sCubeMap = fragmentShader.findVariable("sCubeMap"); + cCamera = fragmentShader.findVariable("cCamera"); + cLocalToGlobal = fragmentShader.findVariable("cLocalToGlobal"); + cAlpha = fragmentShader.findVariable("cAlpha"); + sOpacity = fragmentShader.findVariable("sOpacity"); + } + +} diff --git a/src/alternativa/engine3d/materials/FillMaterial.as b/src/alternativa/engine3d/materials/FillMaterial.as new file mode 100644 index 0000000..06dfefc --- /dev/null +++ b/src/alternativa/engine3d/materials/FillMaterial.as @@ -0,0 +1,153 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.Geometry; + + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DProgramType; + import flash.display3D.VertexBuffer3D; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * The materiall fills surface with solid color in light-independent manner. Can draw a Skin with no more than 41 Joints per surface. See Skin.divide() for more details. + * + * @see alternativa.engine3d.objects.Skin#divide() + */ + public class FillMaterial extends Material { + + private static var caches:Dictionary = new Dictionary(true); + private var cachedContext3D:Context3D; + private var programsCache:Dictionary; + + private static var outColorProcedure:Procedure = new Procedure(["#c0=cColor", "mov o0, c0"], "outColorProcedure"); + + /** + * Transparency + */ + public var alpha:Number = 1; + + private var red:Number; + private var green:Number; + private var blue:Number; + + /** + * Color. + */ + public function get color():uint { + return (red*0xFF << 16) + (green*0xFF << 8) + blue*0xFF; + } + + /** + * @private + */ + public function set color(value:uint):void { + red = ((value >> 16) & 0xFF)/0xFF; + green = ((value >> 8) & 0xFF)/0xFF; + blue = (value & 0xff)/0xFF; + } + + /** + * Creates a new FillMaterial instance. + * @param color Color . + * @param alpha Transparency. + */ + public function FillMaterial(color:uint = 0x7F7F7F, alpha:Number = 1) { + this.color = color; + this.alpha = alpha; + } + + private function setupProgram(object:Object3D):ShaderProgram { + var vertexLinker:Linker = new Linker(Context3DProgramType.VERTEX); + var positionVar:String = "aPosition"; + vertexLinker.declareVariable(positionVar, VariableType.ATTRIBUTE); + if (object.transformProcedure != null) { + positionVar = appendPositionTransformProcedure(object.transformProcedure, vertexLinker); + } + vertexLinker.addProcedure(_projectProcedure); + vertexLinker.setInputParams(_projectProcedure, positionVar); + + var fragmentLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + fragmentLinker.addProcedure(outColorProcedure); + fragmentLinker.varyings = vertexLinker.varyings; + return new ShaderProgram(vertexLinker, fragmentLinker); + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, objectRenderPriority:int = -1):void { + var object:Object3D = surface.object; + // Strams + var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + // Check validity + if (positionBuffer == null) return; + // Program + + // Renew program cache for this context + if (camera.context3D != cachedContext3D) { + cachedContext3D = camera.context3D; + programsCache = caches[cachedContext3D]; + if (programsCache == null) { + programsCache = new Dictionary(); + caches[cachedContext3D] = programsCache; + } + } + + var program:ShaderProgram = programsCache[object.transformProcedure]; + if (program == null) { + program = setupProgram(object); + program.upload(camera.context3D); + programsCache[object.transformProcedure] = program; + } + // Drawcall + var drawUnit:DrawUnit = camera.renderer.createDrawUnit(object, program.program, geometry._indexBuffer, surface.indexBegin, surface.numTriangles, program); + // Streams + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aPosition"), positionBuffer, geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + // Constants + object.setTransformConstants(drawUnit, surface, program.vertexShader, camera); + drawUnit.setProjectionConstants(camera, program.vertexShader.getVariableIndex("cProjMatrix"), object.localToCameraTransform); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cColor"), red, green, blue, alpha); + // Send to render + if (alpha < 1) { + drawUnit.blendSource = Context3DBlendFactor.SOURCE_ALPHA; + drawUnit.blendDestination = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA; + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.TRANSPARENT_SORT); + } else { + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.OPAQUE); + } + } + + /** + * @inheritDoc + */ + override public function clone():Material { + var res:FillMaterial = new FillMaterial(color, alpha); + res.clonePropertiesFrom(this); + return res; + } + + } +} diff --git a/src/alternativa/engine3d/materials/FogMode.as b/src/alternativa/engine3d/materials/FogMode.as new file mode 100644 index 0000000..476e498 --- /dev/null +++ b/src/alternativa/engine3d/materials/FogMode.as @@ -0,0 +1,24 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + + /** + * @private + */ + public class FogMode { + + public static const DISABLED:int = 0; + public static const SIMPLE:int = 1; + public static const ADVANCED:int = 2; + + } + +} diff --git a/src/alternativa/engine3d/materials/LightMapMaterial.as b/src/alternativa/engine3d/materials/LightMapMaterial.as new file mode 100644 index 0000000..4a906ee --- /dev/null +++ b/src/alternativa/engine3d/materials/LightMapMaterial.as @@ -0,0 +1,256 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.Geometry; + import alternativa.engine3d.resources.TextureResource; + + import avmplus.getQualifiedClassName; + + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DProgramType; + import flash.display3D.VertexBuffer3D; + import flash.utils.Dictionary; + import flash.utils.getDefinitionByName; + + use namespace alternativa3d; + + /** + * Texture material which supports light map. Can draw a Skin with no more than 41 Joints per surface. See Skin.divide() for more details. + * To be drawn with this material, geometry should have UV coordinates. Different UV-channels can be used for diffuse texture and light map. + * + * @see alternativa.engine3d.objects.Skin#divide() + * @see alternativa.engine3d.core.VertexAttributes#TEXCOORDS + */ + public class LightMapMaterial extends TextureMaterial { + + private static var caches:Dictionary = new Dictionary(true); + private var cachedContext3D:Context3D; + private var programsCache:Dictionary; + + // inputs: color + private static const _applyLightMapProcedure:Procedure = new Procedure([ + "#v0=vUV1", + "#s0=sLightMap", + "tex t0, v0, s0 <2d,repeat,linear,miplinear>", + "add t0, t0, t0", + "mul i0.xyz, i0.xyz, t0.xyz", + "mov o0, i0" + ], "applyLightMapProcedure"); + + private static const _passLightMapUVProcedure:Procedure = new Procedure([ + "#a0=aUV1", + "#v0=vUV1", + "mov v0, a0" + ], "passLightMapUVProcedure"); + + /** + * Light map. + */ + public var lightMap:TextureResource; + /** + * Number of the UV-channel for light map. + */ + public var lightMapChannel:uint = 0; + + /** + * Creates a new LightMapMaterial instance. + * @param diffuseMap Diffuse texture. + * @param lightMap Light map. + * @param lightMapChannel Number of the UV-channel for light map. + */ + public function LightMapMaterial(diffuseMap:TextureResource = null, lightMap:TextureResource = null, lightMapChannel:uint = 0, opacityMap:TextureResource = null) { + super(diffuseMap, opacityMap); + this.lightMap = lightMap; + this.lightMapChannel = lightMapChannel; + } + + /** + * @inheritDoc + */ + override public function clone():Material { + var res:LightMapMaterial = new LightMapMaterial(diffuseMap, lightMap, lightMapChannel, opacityMap); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @private + */ + override alternativa3d function fillResources(resources:Dictionary, resourceType:Class):void { + super.fillResources(resources, resourceType); + + if (lightMap != null && + A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(lightMap)) as Class, resourceType)) { + resources[lightMap] = true; + } + } + + /** + * @param object + * @param programs + * @param camera + * @param opacityMap + * @param alphaTest 0 - disabled, 1 - opaque, 2 - contours + * @return + */ + private function getProgram(object:Object3D, programs:Vector., camera:Camera3D, opacityMap:TextureResource, alphaTest:int):ShaderProgram { + var key:int = (opacityMap != null ? 3 : 0) + alphaTest; + var program:ShaderProgram = programs[key]; + if (program == null) { + // Make program + // Vertex shader + var vertexLinker:Linker = new Linker(Context3DProgramType.VERTEX); + + var positionVar:String = "aPosition"; + vertexLinker.declareVariable(positionVar, VariableType.ATTRIBUTE); + if (object.transformProcedure != null) { + positionVar = appendPositionTransformProcedure(object.transformProcedure, vertexLinker); + } + vertexLinker.addProcedure(_projectProcedure); + vertexLinker.setInputParams(_projectProcedure, positionVar); + vertexLinker.addProcedure(_passUVProcedure); + vertexLinker.addProcedure(_passLightMapUVProcedure); + + // Pixel shader + var fragmentLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + fragmentLinker.declareVariable("tColor"); + var outProcedure:Procedure = (opacityMap != null ? getDiffuseOpacityProcedure : getDiffuseProcedure); + fragmentLinker.addProcedure(outProcedure); + fragmentLinker.setOutputParams(outProcedure, "tColor"); + + if (alphaTest > 0) { + outProcedure = alphaTest == 1 ? thresholdOpaqueAlphaProcedure : thresholdTransparentAlphaProcedure; + fragmentLinker.addProcedure(outProcedure, "tColor"); + fragmentLinker.setOutputParams(outProcedure, "tColor"); + } + + fragmentLinker.addProcedure(_applyLightMapProcedure, "tColor"); + + fragmentLinker.varyings = vertexLinker.varyings; + + program = new ShaderProgram(vertexLinker, fragmentLinker); + + program.upload(camera.context3D); + programs[key] = program; + } + return program; + } + + private function getDrawUnit(program:ShaderProgram, camera:Camera3D, surface:Surface, geometry:Geometry, opacityMap:TextureResource):DrawUnit { + var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]); + var lightMapUVBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[lightMapChannel]); + + var object:Object3D = surface.object; + + // Drawcall + var drawUnit:DrawUnit = camera.renderer.createDrawUnit(object, program.program, geometry._indexBuffer, surface.indexBegin, surface.numTriangles, program); + + // Streams + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aPosition"), positionBuffer, geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aUV"), uvBuffer, geometry._attributesOffsets[VertexAttributes.TEXCOORDS[0]], VertexAttributes.FORMATS[VertexAttributes.TEXCOORDS[0]]); + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aUV1"), lightMapUVBuffer, geometry._attributesOffsets[VertexAttributes.TEXCOORDS[lightMapChannel]], VertexAttributes.FORMATS[VertexAttributes.TEXCOORDS[lightMapChannel]]); + // Constants + object.setTransformConstants(drawUnit, surface, program.vertexShader, camera); + drawUnit.setProjectionConstants(camera, program.vertexShader.getVariableIndex("cProjMatrix"), object.localToCameraTransform); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cThresholdAlpha"), alphaThreshold, 0, 0, alpha); + // Textures + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sDiffuse"), diffuseMap._texture); + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sLightMap"), lightMap._texture); + if (opacityMap != null) { + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sOpacity"), opacityMap._texture); + } + + return drawUnit; + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, objectRenderPriority:int = -1):void { + if (diffuseMap == null || lightMap == null || diffuseMap._texture == null || lightMap._texture == null) return; + if (opacityMap != null && opacityMap._texture == null) return; + + var object:Object3D = surface.object; + + // Buffers + var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]); + var lightMapUVBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[lightMapChannel]); + + if (positionBuffer == null || uvBuffer == null || lightMapUVBuffer == null) return; + + if (camera.context3D != cachedContext3D) { + cachedContext3D = camera.context3D; + programsCache = caches[cachedContext3D]; + if (programsCache == null) { + programsCache = new Dictionary(); + caches[cachedContext3D] = programsCache; + } + } + + var optionsPrograms:Vector. = programsCache[object.transformProcedure]; + if(optionsPrograms == null) { + optionsPrograms = new Vector.(6, true); + programsCache[object.transformProcedure] = optionsPrograms; + } + + var program:ShaderProgram; + var drawUnit:DrawUnit; + // Opaque pass + if (opaquePass && alphaThreshold <= alpha) { + if (alphaThreshold > 0) { + // Alpha test + // use opacityMap if it is presented + program = getProgram(object, optionsPrograms, camera, opacityMap, 1); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap); + } else { + // do not use opacityMap at all + program = getProgram(object, optionsPrograms, camera, null, 0); + drawUnit = getDrawUnit(program, camera, surface, geometry, null); + } + // Use z-buffer within DrawCall, draws without blending + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.OPAQUE); + } + // Transparent pass + if (transparentPass && alphaThreshold > 0 && alpha > 0) { + // use opacityMap if it is presented + if (alphaThreshold <= alpha && !opaquePass) { + // Alpha threshold + program = getProgram(object, optionsPrograms, camera, opacityMap, 2); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap); + } else { + // There is no Alpha threshold or check z-buffer by previous pass + program = getProgram(object, optionsPrograms, camera, opacityMap, 0); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap); + } + // Do not use z-buffer, draws with blending + drawUnit.blendSource = Context3DBlendFactor.SOURCE_ALPHA; + drawUnit.blendDestination = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA; + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.TRANSPARENT_SORT); + } + } + + } +} diff --git a/src/alternativa/engine3d/materials/Material.as b/src/alternativa/engine3d/materials/Material.as new file mode 100644 index 0000000..6fe8eae --- /dev/null +++ b/src/alternativa/engine3d/materials/Material.as @@ -0,0 +1,116 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Resource; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.Geometry; + + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * Base class for all materials. Material defines, in which way surface will be visualized. + */ + public class Material { + + /** + * @private + */ + alternativa3d function get canDrawInShadowMap():Boolean {return true} + + /** + * Name of the material + */ + public var name:String; + + /** + * @private + */ + alternativa3d static const _projectProcedure:Procedure = getPojectProcedure(); + + private static function getPojectProcedure():Procedure { + var res:Procedure = new Procedure(["m44 o0, i0, c0"], "projectProcedure"); + res.assignVariableName(VariableType.CONSTANT, 0, "cProjMatrix", 4); + return res; + } + + public function Material() { + } + + /** + * @private + */ + alternativa3d function appendPositionTransformProcedure(transformProcedure:Procedure, vertexShader:Linker):String { + vertexShader.declareVariable("tTransformedPosition"); + vertexShader.addProcedure(transformProcedure); + vertexShader.setInputParams(transformProcedure, "aPosition"); + vertexShader.setOutputParams(transformProcedure, "tTransformedPosition"); + return "tTransformedPosition"; + } + + /** + * Gather resources used by material for uploading into context3D. + * + * @param resourceType Gather the resources given type only. + * @return Vector consists of resources. + * @see flash.display.Stage3D + */ + public function getResources(resourceType:Class = null):Vector. { + var res:Vector. = new Vector.(); + var dict:Dictionary = new Dictionary(); + var count:int = 0; + fillResources(dict, resourceType); + for (var key:* in dict) { + res[count++] = key as Resource; + } + return res; + } + + /** + * @private + */ + alternativa3d function fillResources(resources:Dictionary, resourceType:Class):void { + } + + /** + * @private + */ + alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, objectRenderPriority:int = -1):void { + } + + /** + * Duplicates an instance of a Material. + * @return A new Material object that is identical to the original. + */ + public function clone():Material { + var res:Material = new Material(); + res.clonePropertiesFrom(this); + return res; + } + + /** + * Duplicates basic properties. Invoked byclone(). + * @param source Source of properties to be copied from. + */ + protected function clonePropertiesFrom(source:Material):void { + name = source.name; + } + + } +} diff --git a/src/alternativa/engine3d/materials/NormalMapSpace.as b/src/alternativa/engine3d/materials/NormalMapSpace.as new file mode 100644 index 0000000..f0dd850 --- /dev/null +++ b/src/alternativa/engine3d/materials/NormalMapSpace.as @@ -0,0 +1,34 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + + /** + * NormalMapSpace offers constant values that can be used for the normalMapSpace property of materials which use normal map. + * + * @see StandardMaterial#normalMapSpace + */ + public class NormalMapSpace { + + /** + * Normal map defined in surface space, y-axis oriented on top. + */ + public static const TANGENT_RIGHT_HANDED:int = 0; + /** + * Normal map defined in surface space, y-axis oriented on bottom. + */ + public static const TANGENT_LEFT_HANDED:int = 1; + /** + * Normal map defined in object space. + */ + public static const OBJECT:int = 2; + + } +} diff --git a/src/alternativa/engine3d/materials/ShaderProgram.as b/src/alternativa/engine3d/materials/ShaderProgram.as new file mode 100644 index 0000000..9645fb5 --- /dev/null +++ b/src/alternativa/engine3d/materials/ShaderProgram.as @@ -0,0 +1,59 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.materials.compiler.Linker; + + import flash.display3D.Context3D; + import flash.display3D.Program3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class ShaderProgram { + + public var program:Program3D; + + public var vertexShader:Linker; + public var fragmentShader:Linker; + + public function ShaderProgram(vertexShader:Linker, fragmentShader:Linker) { + this.vertexShader = vertexShader; + this.fragmentShader = fragmentShader; + } + + public function upload(context3D:Context3D):void { + if (program != null) program.dispose(); + if (vertexShader != null && fragmentShader != null) { + vertexShader.link(); + fragmentShader.link(); + program = context3D.createProgram(); + try { + program.upload(vertexShader.data, fragmentShader.data); + } catch (e:Error) { + throw (e); + } + } else { + program = null; + } + } + + public function dispose():void { + if (program != null) { + program.dispose(); + program = null; + } + } + + } +} diff --git a/src/alternativa/engine3d/materials/StandardMaterial.as b/src/alternativa/engine3d/materials/StandardMaterial.as new file mode 100644 index 0000000..7530de6 --- /dev/null +++ b/src/alternativa/engine3d/materials/StandardMaterial.as @@ -0,0 +1,939 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.lights.DirectionalLight; + import alternativa.engine3d.lights.OmniLight; + import alternativa.engine3d.lights.SpotLight; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.BitmapTextureResource; + import alternativa.engine3d.resources.Geometry; + import alternativa.engine3d.resources.TextureResource; + + import avmplus.getQualifiedClassName; + + import flash.display.BitmapData; + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DProgramType; + import flash.display3D.Context3DVertexBufferFormat; + import flash.display3D.VertexBuffer3D; + import flash.utils.Dictionary; + import flash.utils.getDefinitionByName; + + use namespace alternativa3d; + + /** + * Material with diffuse, normal, opacity, specular maps and glossiness value. The material is able to draw skin + * with the number of bones in surface no more than 41. To reduce the number of bones in surface can break + * the skin for more surface with fewer bones. Use the method Skin.divide (). To be drawn with this material, + * geometry should have UV coordinates vertex normals and tangent and binormal values​​. + * + * @see alternativa.engine3d.core.VertexAttributes#TEXCOORDS + * @see alternativa.engine3d.core.VertexAttributes#NORMAL + * @see alternativa.engine3d.core.VertexAttributes#TANGENT4 + * @see alternativa.engine3d.objects.Skin#divide() + */ + public class StandardMaterial extends TextureMaterial { + + private static var caches:Dictionary = new Dictionary(true); + private var cachedContext3D:Context3D; + private var programsCache:Dictionary; + + /** + * @private + */ + alternativa3d static const DISABLED:int = 0; + /** + * @private + */ + alternativa3d static const SIMPLE:int = 1; + /** + * @private + */ + alternativa3d static const ADVANCED:int = 2; + + /** + * @private + */ + alternativa3d static var fogMode:int = DISABLED; + /** + * @private + */ + alternativa3d static var fogNear:Number = 1000; + /** + * @private + */ + alternativa3d static var fogFar:Number = 5000; + + /** + * @private + */ + alternativa3d static var fogMaxDensity:Number = 1; + + /** + * @private + */ + alternativa3d static var fogColorR:Number = 0xC8/255; + /** + * @private + */ + alternativa3d static var fogColorG:Number = 0xA2/255; + /** + * @private + */ + alternativa3d static var fogColorB:Number = 0xC8/255; + + /** + * @private + */ + alternativa3d static var fogTexture:TextureResource; + //light procedure caching. The key is light3d instance. + private static const _lightFragmentProcedures:Dictionary = new Dictionary(); + + // inputs : position + private static const _passVaryingsProcedure:Procedure = new Procedure([ + "#v0=vPosition", + "#v1=vViewVector", + "#c0=cCameraPosition", + // Pass the position + "mov v0, i0", + // Vector to Camera + "sub t0, c0, i0", + "mov v1.xyz, t0.xyz", + "mov v1.w, c0.w" + ]); + + // inputs : tangent, normal + private static const _passTBNRightProcedure:Procedure = getPassTBNProcedure(true); + private static const _passTBNLeftProcedure:Procedure = getPassTBNProcedure(false); + private static function getPassTBNProcedure(right:Boolean):Procedure { + var crsInSpace:String = (right) ? "crs t1.xyz, i0, i1" : "crs t1.xyz, i1, i0"; + return new Procedure([ + "#v0=vTangent", + "#v1=vBinormal", + "#v2=vNormal", + // Calculate binormal + crsInSpace, + "mul t1.xyz, t1.xyz, i0.w", + // Transpose normal matrix + "mov v0.x, i0.x", + "mov v0.y, t1.x", + "mov v0.z, i1.x", + "mov v0.w, i1.w", + "mov v1.x, i0.y", + "mov v1.y, t1.y", + "mov v1.z, i1.y", + "mov v1.w, i1.w", + "mov v2.x, i0.z", + "mov v2.y, t1.z", + "mov v2.z, i1.z", + "mov v2.w, i1.w" + ], "passTBNProcedure"); + } + + // outputs : light, highlight + private static const _ambientLightProcedure:Procedure = new Procedure([ + "#c0=cSurface", + "mov o0, i0", + "mov o1, c0.xxxx" + ], "ambientLightProcedure"); + + // Set o.w to glossiness + private static const _setGlossinessFromConstantProcedure:Procedure = new Procedure([ + "#c0=cSurface", + "mov o0.w, c0.y" + ], "setGlossinessFromConstantProcedure"); + // Set o.w to glossiness from texture + private static const _setGlossinessFromTextureProcedure:Procedure = new Procedure([ + "#v0=vUV", + "#c0=cSurface", + "#s0=sGlossiness", + "tex t0, v0, s0 <2d, repeat, linear, miplinear>", + "mul o0.w, t0.x, c0.y" + ], "setGlossinessFromTextureProcedure"); + + // outputs : normal, viewVector + private static const _getNormalAndViewTangentProcedure:Procedure = new Procedure([ + "#v0=vTangent", + "#v1=vBinormal", + "#v2=vNormal", + "#v3=vUV", + "#v4=vViewVector", + "#c0=cAmbientColor", + "#s0=sBump", + // Extract normal from the texture + "tex t0, v3, s0 <2d,repeat,linear,miplinear>", + "add t0, t0, t0", + "sub t0.xyz, t0.xyz, c0.www", + // Transform the normal with TBN + "nrm t1.xyz, v0.xyz", + "dp3 o0.x, t0.xyz, t1.xyz", + "nrm t1.xyz, v1.xyz", + "dp3 o0.y, t0.xyz, t1.xyz", + "nrm t1.xyz, v2.xyz", + "dp3 o0.z, t0.xyz, t1.xyz", + // Normalization + "nrm o0.xyz, o0.xyz", + // Returns normalized vector of view + "nrm o1.xyz, v4" + ], "getNormalAndViewTangentProcedure"); + // outputs : normal, viewVector + private static const _getNormalAndViewObjectProcedure:Procedure = new Procedure([ + "#v3=vUV", + "#v4=vViewVector", + "#c0=cAmbientColor", + "#s0=sBump", + // Extract normal from the texture + "tex t0, v3, s0 <2d,repeat,linear,miplinear>", + "add t0, t0, t0", + "sub t0.xyz, t0.xyz, c0.www", + // Normalization + "nrm o0.xyz, t0.xyz", + // Returns normalized vector of view + "nrm o1.xyz, v4" + ], "getNormalAndViewObjectProcedure"); + + // Apply specular map color to a flare + private static const _applySpecularProcedure:Procedure = new Procedure([ + "#v0=vUV", + "#s0=sSpecular", + "tex t0, v0, s0 <2d, repeat,linear,miplinear>", + "mul o0.xyz, o0.xyz, t0.xyz" + ], "applySpecularProcedure"); + + //Apply light and flare to diffuse + // inputs : "diffuse", "tTotalLight", "tTotalHighLight" + private static const _mulLightingProcedure:Procedure = new Procedure([ + "#c0=cSurface", // c0.z - specularPower + "mul i0.xyz, i0.xyz, i1.xyz", + "mul t1.xyz, i2.xyz, c0.z", + "add i0.xyz, i0.xyz, t1.xyz", + "mov o0, i0" + ], "mulLightingProcedure"); + + // inputs : position + private static const passSimpleFogConstProcedure:Procedure = new Procedure([ + "#v0=vZDistance", + "#c0=cFogSpace", + "dp4 t0.z, i0, c0", + "mov v0, t0.zzzz", + "sub v0.y, i0.w, t0.z" + ], "passSimpleFogConst"); + + // inputs : color + private static const outputWithSimpleFogProcedure:Procedure = new Procedure([ + "#v0=vZDistance", + "#c0=cFogColor", + "#c1=cFogRange", + // Restrict fog factor with the range + "min t0.xy, v0.xy, c1.xy", + "max t0.xy, t0.xy, c1.zw", + "mul i0.xyz, i0.xyz, t0.y", + "mul t0.xyz, c0.xyz, t0.x", + "add i0.xyz, i0.xyz, t0.xyz", + "mov o0, i0" + ], "outputWithSimpleFog"); + + // inputs : position, projected + private static const postPassAdvancedFogConstProcedure:Procedure = new Procedure([ + "#v0=vZDistance", + "#c0=cFogSpace", + "dp4 t0.z, i0, c0", + "mov v0, t0.zzzz", + "sub v0.y, i0.w, t0.z", + // Screen x coordinate + "mov v0.zw, i1.xwxw", + "mov o0, i1" + ], "postPassAdvancedFogConst"); + + // inputs : color + private static const outputWithAdvancedFogProcedure:Procedure = new Procedure([ + "#v0=vZDistance", + "#c0=cFogConsts", + "#c1=cFogRange", + "#s0=sFogTexture", + // Restrict fog factor with the range + "min t0.xy, v0.xy, c1.xy", + "max t0.xy, t0.xy, c1.zw", + "mul i0.xyz, i0.xyz, t0.y", + // Calculate fog color + "mov t1.xyzw, c0.yyzw", + "div t0.z, v0.z, v0.w", + "mul t0.z, t0.z, c0.x", + "add t1.x, t1.x, t0.z", + "tex t1, t1, s0 <2d, repeat, linear, miplinear>", + "mul t0.xyz, t1.xyz, t0.x", + "add i0.xyz, i0.xyz, t0.xyz", + "mov o0, i0" + ], "outputWithAdvancedFog"); + + // Add lightmap value with light + private static const _addLightMapProcedure:Procedure = new Procedure([ + "#v0=vUV1", + "#s0=sLightMap", + "tex t0, v0, s0 <2d,repeat,linear,miplinear>", + "add t0, t0, t0", + "add o0.xyz, i0.xyz, t0.xyz" + ], "applyLightMapProcedure"); + + private static const _passLightMapUVProcedure:Procedure = new Procedure([ + "#a0=aUV1", + "#v0=vUV1", + "mov v0, a0" + ], "passLightMapUVProcedure"); + + /** + * Normal map. + */ + public var normalMap:TextureResource; + + private var _normalMapSpace:int = NormalMapSpace.TANGENT_RIGHT_HANDED; + /** + * Type of the normal map. Should be defined by constants of NormalMapSpace class. + * + * @default NormalMapSpace.TANGENT + * + * @see NormalMapSpace + */ + public function get normalMapSpace():int { + return _normalMapSpace; + } + + /** + * @private + */ + public function set normalMapSpace(value:int):void { + if (value != NormalMapSpace.TANGENT_RIGHT_HANDED && value != NormalMapSpace.TANGENT_LEFT_HANDED && value != NormalMapSpace.OBJECT) { + throw new ArgumentError("Value must be a constant from the NormalMapSpace class"); + } + _normalMapSpace = value; + } + + /** + * Specular map. + */ + public var specularMap:TextureResource; + /** + * Glossiness map. + */ + public var glossinessMap:TextureResource; + + /** + * Light map. + */ + public var lightMap:TextureResource; + + /** + * Number of the UV-channel for light map. + */ + public var lightMapChannel:uint = 0; + /** + * Glossiness. Multiplies with glossinessMap value. + */ + public var glossiness:Number = 100; + + /** + * Brightness of a flare. Multiplies with specularMap value. + */ + public var specularPower:Number = 1; + + /** + * Creates a new StandardMaterial instance. + * @param diffuseMap Diffuse map. + * @param normalMap Normal map. + * @param specularMap Specular map. + * @param glossinessMap Glossiness map. + * @param opacityMap Opacity map. + */ + public function StandardMaterial(diffuseMap:TextureResource = null, normalMap:TextureResource = null, specularMap:TextureResource = null, glossinessMap:TextureResource = null, opacityMap:TextureResource = null) { + super(diffuseMap, opacityMap); + this.normalMap = normalMap; + this.specularMap = specularMap; + this.glossinessMap = glossinessMap; + } + + /** + * @private + */ + override alternativa3d function fillResources(resources:Dictionary, resourceType:Class):void { + super.fillResources(resources, resourceType); + if (normalMap != null && + A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(normalMap)) as Class, resourceType)) { + resources[normalMap] = true; + } + + if (lightMap != null && + A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(lightMap)) as Class, resourceType)) { + resources[lightMap] = true; + } + + if (glossinessMap != null && + A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(glossinessMap)) as Class, resourceType)) { + resources[glossinessMap] = true; + } + + if (specularMap != null && + A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(specularMap)) as Class, resourceType)) { + resources[specularMap] = true; + } + } + + /** + * @private + */ + alternativa3d function getPassUVProcedure():Procedure { + return _passUVProcedure; + } + + /** + * @private + */ + alternativa3d function setPassUVProcedureConstants(destination:DrawUnit, vertexLinker:Linker):void { + } + + // inputs: tNormal", "tViewVector", "shadow", "cAmbientColor" + // outputs : light, hightlight + private function formDirectionalProcedure(procedure:Procedure, light:Light3D, useShadow:Boolean):void { + var source:Array = [ + "#c0=c" + light.lightID + "Direction", + "#c1=c" + light.lightID + "Color", + // Calculate half-way vector + "add t0.xyz, i1.xyz, c0.xyz", + "mov t0.w, c0.w", + "nrm t0.xyz,t0.xyz", + // Calculate a flare + "dp3 t0.w, t0.xyz, i0.xyz", + "pow t0.w, t0.w, o1.w", + // Calculate light + "dp3 t0.x, i0.xyz, c0.xyz", + "sat t0.x, t0.x", + ]; + if (useShadow) { + source.push("mul t0.x, t0.x, i2.x"); + source.push("mul t0.xyz, c1.xyz, t0.xxx"); + source.push("add o0.xyz, t0.xyz, i3.xyz"); + source.push("mul t0.w, i2.x, t0.w"); + source.push("mul o1.xyz, c1.xyz, t0.www"); + } else { + // Apply calculated values + source.push("mul t0.xyz, c1.xyz, t0.xxxx"); + source.push("add o0, o0, t0.xyz"); + source.push("mul t0.xyz, c1.xyz, t0.w"); + source.push("add o1.xyz, o1.xyz, t0.xyz"); + } + procedure.compileFromArray(source); + } + + /** + * @param object + * @param materialKey + * @param opacityMap + * @param alphaTest 0:disabled 1:alpha-test 2:contours + * @param lights + * @param directionalLight + * @param lightsLength + */ + private function getProgram(object:Object3D, programs:Dictionary, camera:Camera3D, materialKey:String, opacityMap:TextureResource, alphaTest:int, lights:Vector., lightsLength:int, shadowedLight:Light3D):ShaderProgram { + var key:String = materialKey + (opacityMap != null ? "O" : "o") + alphaTest.toString(); + var program:ShaderProgram = programs[key]; + if (program == null) { + var vertexLinker:Linker = new Linker(Context3DProgramType.VERTEX); + var fragmentLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + var i:int; + + fragmentLinker.declareVariable("tTotalLight"); + fragmentLinker.declareVariable("tTotalHighLight"); + fragmentLinker.declareVariable("tNormal"); + fragmentLinker.declareVariable("cAmbientColor", VariableType.CONSTANT); + fragmentLinker.addProcedure(_ambientLightProcedure); + fragmentLinker.setInputParams(_ambientLightProcedure, "cAmbientColor"); + fragmentLinker.setOutputParams(_ambientLightProcedure, "tTotalLight", "tTotalHighLight"); + var positionVar:String = "aPosition"; + var normalVar:String = "aNormal"; + var tangentVar:String = "aTangent"; + vertexLinker.declareVariable(positionVar, VariableType.ATTRIBUTE); + vertexLinker.declareVariable(tangentVar, VariableType.ATTRIBUTE); + vertexLinker.declareVariable(normalVar, VariableType.ATTRIBUTE); + if (object.transformProcedure != null) { + positionVar = appendPositionTransformProcedure(object.transformProcedure, vertexLinker); + } + + vertexLinker.addProcedure(_projectProcedure); + vertexLinker.setInputParams(_projectProcedure, positionVar); + + vertexLinker.addProcedure(getPassUVProcedure()); + + if (glossinessMap != null) { + fragmentLinker.addProcedure(_setGlossinessFromTextureProcedure); + fragmentLinker.setOutputParams(_setGlossinessFromTextureProcedure, "tTotalHighLight"); + } else { + fragmentLinker.addProcedure(_setGlossinessFromConstantProcedure); + fragmentLinker.setOutputParams(_setGlossinessFromConstantProcedure, "tTotalHighLight"); + } + if (lightsLength > 0) { + var procedure:Procedure; + if (object.deltaTransformProcedure != null) { + vertexLinker.declareVariable("tTransformedNormal"); + procedure = object.deltaTransformProcedure.newInstance(); + vertexLinker.addProcedure(procedure); + vertexLinker.setInputParams(procedure, normalVar); + vertexLinker.setOutputParams(procedure, "tTransformedNormal"); + normalVar = "tTransformedNormal"; + + vertexLinker.declareVariable("tTransformedTangent"); + procedure = object.deltaTransformProcedure.newInstance(); + vertexLinker.addProcedure(procedure); + vertexLinker.setInputParams(procedure, tangentVar); + vertexLinker.setOutputParams(procedure, "tTransformedTangent"); + tangentVar = "tTransformedTangent"; + } + vertexLinker.addProcedure(_passVaryingsProcedure); + vertexLinker.setInputParams(_passVaryingsProcedure, positionVar); + fragmentLinker.declareVariable("tViewVector"); + + if (_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED || _normalMapSpace == NormalMapSpace.TANGENT_LEFT_HANDED) { + var nrmProcedure:Procedure = (_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED) ? _passTBNRightProcedure : _passTBNLeftProcedure; + vertexLinker.addProcedure(nrmProcedure); + vertexLinker.setInputParams(nrmProcedure, tangentVar, normalVar); + fragmentLinker.addProcedure(_getNormalAndViewTangentProcedure); + fragmentLinker.setOutputParams(_getNormalAndViewTangentProcedure, "tNormal", "tViewVector"); + } else { + fragmentLinker.addProcedure(_getNormalAndViewObjectProcedure); + fragmentLinker.setOutputParams(_getNormalAndViewObjectProcedure, "tNormal", "tViewVector"); + } + if (shadowedLight != null && shadowedLight is DirectionalLight) { + vertexLinker.addProcedure(shadowedLight.shadow.vertexShadowProcedure, positionVar); + var shadowProc:Procedure = shadowedLight.shadow.fragmentShadowProcedure; + fragmentLinker.addProcedure(shadowProc); + fragmentLinker.setOutputParams(shadowProc, "tTotalLight"); + + var dirMulShadowProcedure:Procedure = _lightFragmentProcedures[shadowedLight.shadow]; + if (dirMulShadowProcedure == null) { + dirMulShadowProcedure = new Procedure(); + formDirectionalProcedure(dirMulShadowProcedure, shadowedLight, true); + } + fragmentLinker.addProcedure(dirMulShadowProcedure); + fragmentLinker.setInputParams(dirMulShadowProcedure, "tNormal", "tViewVector", "tTotalLight", "cAmbientColor"); + fragmentLinker.setOutputParams(dirMulShadowProcedure, "tTotalLight", "tTotalHighLight"); + } + + for (i = 0; i < lightsLength; i++) { + var light:Light3D = lights[i]; + if (light == shadowedLight) continue; + var lightFragmentProcedure:Procedure = _lightFragmentProcedures[light]; + if (lightFragmentProcedure == null) { + lightFragmentProcedure = new Procedure(); + lightFragmentProcedure.name = "light" + i.toString(); + if (light is DirectionalLight) { + formDirectionalProcedure(lightFragmentProcedure, light, false); + lightFragmentProcedure.name += "Directional"; + } else if (light is OmniLight) { + lightFragmentProcedure.compileFromArray([ + "#c0=c" + light.lightID + "Position", + "#c1=c" + light.lightID + "Color", + "#c2=c" + light.lightID + "Radius", + "#v0=vPosition", + // Calculate vector from the point to light + "sub t0, c0, v0", // L = lightPos - PointPos + "dp3 t0.w, t0.xyz, t0.xyz", // lenSqr + "nrm t0.xyz, t0.xyz", // L = normalize(L) + // Calculate half-way vector + "add t1.xyz, i1.xyz, t0.xyz", + "mov t1.w, c0.w", + "nrm t1.xyz, t1.xyz", + // Calculate a flare + "dp3 t1.w, t1.xyz, i0.xyz", + "pow t1.w, t1.w, o1.w", + // Calculate distance to the light source + "sqt t1.x, t0.w", // len = sqt(lensqr) + // Calculate light + "dp3 t0.w, t0.xyz, i0.xyz", // dot = dot(normal, L) + // Calculate decay + "sub t0.x, t1.x, c2.z", // len = len - atenuationBegin + "div t0.y, t0.x, c2.y", // att = len/radius + "sub t0.x, c2.x, t0.y", // att = 1 - len/radius + "sat t0.xw, t0.xw", // t = max(t, 0) + // Multiply light color with the decay value + "mul t0.xyz, c1.xyz, t0.xxx", // t = color*t + "mul t1.xyz, t0.xyz, t1.w", + "add o1.xyz, o1.xyz, t1.xyz", + "mul t0.xyz, t0.xyz, t0.www", + "add o0.xyz, o0.xyz, t0.xyz" + ]); + lightFragmentProcedure.name += "Omni"; + } else if (light is SpotLight) { + lightFragmentProcedure.compileFromArray([ + "#c0=c" + light.lightID + "Position", + "#c1=c" + light.lightID + "Color", + "#c2=c" + light.lightID + "Radius", + "#c3=c" + light.lightID + "Axis", + "#v0=vPosition", + // Calculate vector from the point to light + "sub t0, c0, v0",// L = pos - lightPos + "dp3 t0.w, t0, t0",// lenSqr + "nrm t0.xyz,t0.xyz",// L = normalize(L) + // Calculate half-way vector + "add t2.xyz, i1.xyz, t0.xyz", + "nrm t2.xyz, t2.xyz", + //Calculate a flare + "dp3 t2.x, t2.xyz, i0.xyz", + "pow t2.x, t2.x, o1.w", + "dp3 t1.x, t0.xyz, c3.xyz", //axisDirDot + "dp3 t0.x, t0, i0.xyz",// dot = dot(normal, L) + "sqt t0.w, t0.w",// len = sqt(lensqr) + "sub t0.w, t0.w, c2.y",// len = len - atenuationBegin + "div t0.y, t0.w, c2.x",// att = len/radius + "sub t0.w, c0.w, t0.y",// att = 1 - len/radius + "sub t0.y, t1.x, c2.w", + "div t0.y, t0.y, c2.z", + "sat t0.xyw,t0.xyw",// t = sat(t) + "mul t1.xyz,c1.xyz,t0.yyy",// t = color*t + "mul t1.xyz,t1.xyz,t0.www",// + "mul t2.xyz, t2.x, t1.xyz", + "add o1.xyz, o1.xyz, t2.xyz", + "mul t1.xyz, t1.xyz, t0.xxx", + + "add o0.xyz, o0.xyz, t1.xyz" + ]); + lightFragmentProcedure.name += "Spot"; + } + } + fragmentLinker.addProcedure(lightFragmentProcedure); + fragmentLinker.setInputParams(lightFragmentProcedure, "tNormal", "tViewVector"); + fragmentLinker.setOutputParams(lightFragmentProcedure, "tTotalLight", "tTotalHighLight"); + } + } + + var outputProcedure:Procedure; + if (specularMap != null) { + fragmentLinker.addProcedure(_applySpecularProcedure); + fragmentLinker.setOutputParams(_applySpecularProcedure, "tTotalHighLight"); + outputProcedure = _applySpecularProcedure; + } + if (lightMap != null) { + vertexLinker.addProcedure(_passLightMapUVProcedure); + fragmentLinker.addProcedure(_addLightMapProcedure); + fragmentLinker.setInputParams(_addLightMapProcedure, "tTotalLight"); + fragmentLinker.setOutputParams(_addLightMapProcedure, "tTotalLight"); + } + + fragmentLinker.declareVariable("tColor"); + outputProcedure = opacityMap != null ? getDiffuseOpacityProcedure : getDiffuseProcedure; + fragmentLinker.addProcedure(outputProcedure); + fragmentLinker.setOutputParams(outputProcedure, "tColor"); + + if (alphaTest > 0) { + outputProcedure = alphaTest == 1 ? thresholdOpaqueAlphaProcedure : thresholdTransparentAlphaProcedure; + fragmentLinker.addProcedure(outputProcedure, "tColor"); + fragmentLinker.setOutputParams(outputProcedure, "tColor"); + } + + fragmentLinker.addProcedure(_mulLightingProcedure, "tColor", "tTotalLight", "tTotalHighLight"); + + + if (fogMode == SIMPLE || fogMode == ADVANCED) { + fragmentLinker.setOutputParams(_mulLightingProcedure, "tColor"); + } + if (fogMode == SIMPLE) { + vertexLinker.addProcedure(passSimpleFogConstProcedure); + vertexLinker.setInputParams(passSimpleFogConstProcedure, positionVar); + fragmentLinker.addProcedure(outputWithSimpleFogProcedure); + fragmentLinker.setInputParams(outputWithSimpleFogProcedure, "tColor"); + outputProcedure = outputWithSimpleFogProcedure; + } else if (fogMode == ADVANCED) { + vertexLinker.declareVariable("tProjected"); + vertexLinker.setOutputParams(_projectProcedure, "tProjected"); + vertexLinker.addProcedure(postPassAdvancedFogConstProcedure); + vertexLinker.setInputParams(postPassAdvancedFogConstProcedure, positionVar, "tProjected"); + fragmentLinker.addProcedure(outputWithAdvancedFogProcedure); + fragmentLinker.setInputParams(outputWithAdvancedFogProcedure, "tColor"); + outputProcedure = outputWithAdvancedFogProcedure; + } + + fragmentLinker.varyings = vertexLinker.varyings; + program = new ShaderProgram(vertexLinker, fragmentLinker); + + + program.upload(camera.context3D); + programs[key] = program; + } + return program; + } + + private function getDrawUnit(program:ShaderProgram, camera:Camera3D, surface:Surface, geometry:Geometry, opacityMap:TextureResource, lights:Vector., lightsLength:int, shadowedLight:Light3D):DrawUnit { + // Buffers + var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]); + var normalsBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.NORMAL); + var tangentsBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TANGENT4); + + var object:Object3D = surface.object; + + // Draw call + var drawUnit:DrawUnit = camera.renderer.createDrawUnit(object, program.program, geometry._indexBuffer, surface.indexBegin, surface.numTriangles, program); + + // Streams + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aPosition"), positionBuffer, geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aUV"), uvBuffer, geometry._attributesOffsets[VertexAttributes.TEXCOORDS[0]], VertexAttributes.FORMATS[VertexAttributes.TEXCOORDS[0]]); + + // Constants + object.setTransformConstants(drawUnit, surface, program.vertexShader, camera); + drawUnit.setProjectionConstants(camera, program.vertexShader.getVariableIndex("cProjMatrix"), object.localToCameraTransform); + // Set options for a surface. X should be 0. + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cSurface"), 0, glossiness, specularPower, 1); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cThresholdAlpha"), alphaThreshold, 0, 0, alpha); + + if (lightsLength > 0) { + if (_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED || _normalMapSpace == NormalMapSpace.TANGENT_LEFT_HANDED) { + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aNormal"), normalsBuffer, geometry._attributesOffsets[VertexAttributes.NORMAL], VertexAttributes.FORMATS[VertexAttributes.NORMAL]); + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aTangent"), tangentsBuffer, geometry._attributesOffsets[VertexAttributes.TANGENT4], VertexAttributes.FORMATS[VertexAttributes.TANGENT4]); + } + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sBump"), normalMap._texture); + + var camTransform:Transform3D = object.cameraToLocalTransform; + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("cCameraPosition"), camTransform.d, camTransform.h, camTransform.l); + + var transform:Transform3D; + var rScale:Number; + for (var i:int = 0; i < lightsLength; i++) { + var light:Light3D = lights[i]; + if (light is DirectionalLight) { + transform = light.lightToObjectTransform; + var len:Number = Math.sqrt(transform.c*transform.c + transform.g*transform.g + transform.k*transform.k); + + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + light.lightID + "Direction"), -transform.c/len, -transform.g/len, -transform.k/len, 1); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + light.lightID + "Color"), light.red, light.green, light.blue); + } else if (light is OmniLight) { + var omni:OmniLight = light as OmniLight; + transform = light.lightToObjectTransform; + rScale = Math.sqrt(transform.a*transform.a + transform.e*transform.e + transform.i*transform.i); + rScale += Math.sqrt(transform.b*transform.b + transform.f*transform.f + transform.j*transform.j); + rScale += Math.sqrt(transform.c*transform.c + transform.g*transform.g + transform.k*transform.k); + rScale /= 3; + + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + light.lightID + "Position"), transform.d, transform.h, transform.l); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + light.lightID + "Radius"), 1, omni.attenuationEnd*rScale - omni.attenuationBegin*rScale, omni.attenuationBegin*rScale); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + light.lightID + "Color"), light.red, light.green, light.blue); + } else if (light is SpotLight) { + var spot:SpotLight = light as SpotLight; + transform = light.lightToObjectTransform; + rScale = Math.sqrt(transform.a*transform.a + transform.e*transform.e + transform.i*transform.i); + rScale += Math.sqrt(transform.b*transform.b + transform.f*transform.f + transform.j*transform.j); + rScale += len = Math.sqrt(transform.c*transform.c + transform.g*transform.g + transform.k*transform.k); + rScale /= 3; + var falloff:Number = Math.cos(spot.falloff*0.5); + var hotspot:Number = Math.cos(spot.hotspot*0.5); + + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + light.lightID + "Position"), transform.d, transform.h, transform.l); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + light.lightID + "Axis"), -transform.c/len, -transform.g/len, -transform.k/len); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + light.lightID + "Radius"), spot.attenuationEnd*rScale - spot.attenuationBegin*rScale, spot.attenuationBegin*rScale, hotspot == falloff ? 0.000001 : hotspot - falloff, falloff); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + light.lightID + "Color"), light.red, light.green, light.blue); + } + } + } + + // Textures + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sDiffuse"), diffuseMap._texture); + if (opacityMap != null) { + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sOpacity"), opacityMap._texture); + } + if (glossinessMap != null) { + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sGlossiness"), glossinessMap._texture); + } + if (specularMap != null) { + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sSpecular"), specularMap._texture); + } + + if (lightMap != null) { + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aUV1"), + geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[lightMapChannel]), + geometry._attributesOffsets[VertexAttributes.TEXCOORDS[lightMapChannel]], + Context3DVertexBufferFormat.FLOAT_2); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cAmbientColor"), 0,0,0, 1); + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sLightMap"), lightMap._texture); + } else { + drawUnit.setFragmentConstantsFromVector(program.fragmentShader.getVariableIndex("cAmbientColor"), camera.ambient, 1); + } + setPassUVProcedureConstants(drawUnit, program.vertexShader); + + if (shadowedLight != null && shadowedLight is DirectionalLight) { + shadowedLight.shadow.setup(drawUnit, program.vertexShader, program.fragmentShader, surface); + } + + if (fogMode == SIMPLE || fogMode == ADVANCED) { + var lm:Transform3D = object.localToCameraTransform; + var dist:Number = fogFar - fogNear; + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("cFogSpace"), lm.i/dist, lm.j/dist, lm.k/dist, (lm.l - fogNear)/dist); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cFogRange"), fogMaxDensity, 1, 0, 1 - fogMaxDensity); + } + if (fogMode == SIMPLE) { + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cFogColor"), fogColorR, fogColorG, fogColorB); + } + if (fogMode == ADVANCED) { + if (fogTexture == null) { + var bmd:BitmapData = new BitmapData(32, 1, false, 0xFF0000); + for (i = 0; i < 32; i++) { + bmd.setPixel(i, 0, ((i/32)*255) << 16); + } + fogTexture = new BitmapTextureResource(bmd); + fogTexture.upload(camera.context3D); + } + var cLocal:Transform3D = camera.localToGlobalTransform; + var halfW:Number = camera.view.width/2; + var leftX:Number = -halfW*cLocal.a + camera.focalLength*cLocal.c; + var leftY:Number = -halfW*cLocal.e + camera.focalLength*cLocal.g; + var rightX:Number = halfW*cLocal.a + camera.focalLength*cLocal.c; + var rightY:Number = halfW*cLocal.e + camera.focalLength*cLocal.g; + // Finding UV + var angle:Number = (Math.atan2(leftY, leftX) - Math.PI/2); + if (angle < 0) angle += Math.PI*2; + var dx:Number = rightX - leftX; + var dy:Number = rightY - leftY; + var lens:Number = Math.sqrt(dx*dx + dy*dy); + leftX /= lens; + leftY /= lens; + rightX /= lens; + rightY /= lens; + var uScale:Number = Math.acos(leftX*rightX + leftY*rightY)/Math.PI/2; + var uRight:Number = angle/Math.PI/2; + + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cFogConsts"), 0.5*uScale, 0.5 - uRight, 0); + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sFogTexture"), fogTexture._texture); + } + return drawUnit; + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, objectRenderPriority:int = -1):void { + if (diffuseMap == null || normalMap == null || diffuseMap._texture == null || normalMap._texture == null) return; + // Check if textures uploaded in to the context. + if (opacityMap != null && opacityMap._texture == null || glossinessMap != null && glossinessMap._texture == null || specularMap != null && specularMap._texture == null || lightMap != null && lightMap._texture == null) return; + + var object:Object3D = surface.object; + + // Buffers + var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]); + var normalsBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.NORMAL); + var tangentsBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TANGENT4); + + if (positionBuffer == null || uvBuffer == null) return; + + if (lightsLength > 0 && (_normalMapSpace == NormalMapSpace.TANGENT_RIGHT_HANDED || _normalMapSpace == NormalMapSpace.TANGENT_LEFT_HANDED)) { + if (normalsBuffer == null || tangentsBuffer == null) return; + } + + // Make shared part of the key. + var materialKey:String = (fogMode.toString()) + + ((lightMap != null) ? "L" : "l") + + (_normalMapSpace.toString()) + + ((glossinessMap != null) ? "G" : "g") + + ((specularMap != null) ? "S" : "s"); + var shadowedLight:Light3D; + for (var i:int = 0; i < lightsLength; i++) { + var light:Light3D = lights[i]; + if (light.shadow != null && shadowedLight == null) { + shadowedLight = light; + materialKey += light.shadow.type; + } + materialKey += light.lightID; + } + + // Refresh programs for this context. + if (camera.context3D != cachedContext3D) { + cachedContext3D = camera.context3D; + programsCache = caches[cachedContext3D]; + if (programsCache == null) { + programsCache = new Dictionary(); + caches[cachedContext3D] = programsCache; + } + } + + var optionsPrograms:Dictionary = programsCache[object.transformProcedure]; + if (optionsPrograms == null) { + optionsPrograms = new Dictionary(false); + programsCache[object.transformProcedure] = optionsPrograms; + } + + var program:ShaderProgram; + var drawUnit:DrawUnit; + // Opaque pass + if (opaquePass && alphaThreshold <= alpha) { + if (alphaThreshold > 0) { + // Alpha test + // use opacityMap if it is presented + program = getProgram(object, optionsPrograms, camera, materialKey, opacityMap, 1, lights, lightsLength, shadowedLight); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap, lights, lightsLength, shadowedLight); + } else { + // do not use opacityMap at all + program = getProgram(object, optionsPrograms, camera, materialKey, null, 0, lights, lightsLength, shadowedLight); + drawUnit = getDrawUnit(program, camera, surface, geometry, null, lights, lightsLength, shadowedLight); + } + // Use z-buffer within DrawCall, draws without blending + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.OPAQUE); + } + // Transparent pass + if (transparentPass && alphaThreshold > 0 && alpha > 0) { + // use opacityMap if it is presented + if (alphaThreshold <= alpha && !opaquePass) { + // Alpha threshold + program = getProgram(object, optionsPrograms, camera, materialKey, opacityMap, 2, lights, lightsLength, shadowedLight); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap, lights, lightsLength, shadowedLight); + } else { + // There is no Alpha threshold or check z-buffer by previous pass + program = getProgram(object, optionsPrograms, camera, materialKey, opacityMap, 0, lights, lightsLength, shadowedLight); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap, lights, lightsLength, shadowedLight); + } + // Do not use z-buffer, draws with blending + drawUnit.blendSource = Context3DBlendFactor.SOURCE_ALPHA; + drawUnit.blendDestination = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA; + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.TRANSPARENT_SORT); + } + } + + /** + * @inheritDoc + */ + override public function clone():Material { + var res:StandardMaterial = new StandardMaterial(diffuseMap, normalMap, specularMap, glossinessMap, opacityMap); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Material):void { + super.clonePropertiesFrom(source); + var sMaterial:StandardMaterial = StandardMaterial(source); + glossiness = sMaterial.glossiness; + specularPower = sMaterial.specularPower; + _normalMapSpace = sMaterial._normalMapSpace; + lightMap = sMaterial.lightMap; + lightMapChannel = sMaterial.lightMapChannel; + } + + } +} diff --git a/src/alternativa/engine3d/materials/TextureMaterial.as b/src/alternativa/engine3d/materials/TextureMaterial.as new file mode 100644 index 0000000..20008ee --- /dev/null +++ b/src/alternativa/engine3d/materials/TextureMaterial.as @@ -0,0 +1,333 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.Geometry; + import alternativa.engine3d.resources.TextureResource; + + import avmplus.getQualifiedClassName; + + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DProgramType; + import flash.display3D.VertexBuffer3D; + import flash.utils.Dictionary; + import flash.utils.getDefinitionByName; + + use namespace alternativa3d; + + /** + * The materiall fills surface with bitmap image in light-independent manner. Can draw a Skin with no more than 41 Joints per surface. See Skin.divide() for more details. + * + * To be drawn with this material, geometry shoud have UV coordinates. + * @see alternativa.engine3d.objects.Skin#divide() + * @see alternativa.engine3d.core.VertexAttributes#TEXCOORDS + */ + public class TextureMaterial extends Material { + + /** + * @private + */ + alternativa3d override function get canDrawInShadowMap():Boolean { + return opaquePass && alphaThreshold == 0; + } + + private static var caches:Dictionary = new Dictionary(true); + private var cachedContext3D:Context3D; + private var programsCache:Dictionary; + + /** + * @private + * Procedure for diffuse map with alpha channel + */ + static alternativa3d const getDiffuseProcedure:Procedure = new Procedure([ + "#v0=vUV", + "#s0=sDiffuse", + "#c0=cThresholdAlpha", + "tex t0, v0, s0 <2d, linear,repeat, miplinear>", + "mul t0.w, t0.w, c0.w", + "mov o0, t0" + ], "getDiffuseProcedure"); + + /** + * @private + * Procedure for diffuse with opacity map. + */ + static alternativa3d const getDiffuseOpacityProcedure:Procedure = new Procedure([ + "#v0=vUV", + "#s0=sDiffuse", + "#s1=sOpacity", + "#c0=cThresholdAlpha", + "tex t0, v0, s0 <2d, linear,repeat, miplinear>", + "tex t1, v0, s1 <2d, linear,repeat, miplinear>", + "mul t0.w, t1.x, c0.w", + "mov o0, t0" + ], "getDiffuseOpacityProcedure"); + + /** + * @private + * Alpha-test check procedure. + */ + static alternativa3d const thresholdOpaqueAlphaProcedure:Procedure = new Procedure([ + "#c0=cThresholdAlpha", + "sub t0.w, i0.w, c0.x", + "kil t0.w", + "mov o0, i0" + ], "thresholdOpaqueAlphaProcedure"); + + /** + * @private + * Alpha-test check procedure. + */ + static alternativa3d const thresholdTransparentAlphaProcedure:Procedure = new Procedure([ + "#c0=cThresholdAlpha", + "slt t0.w, i0.w, c0.x", + "mul i0.w, t0.w, i0.w", + "mov o0, i0" + ], "thresholdTransparentAlphaProcedure"); + + /** + * @private + * Pass UV to the fragment shader procedure + */ + static alternativa3d const _passUVProcedure:Procedure = new Procedure(["#v0=vUV", "#a0=aUV", "mov v0, a0"], "passUVProcedure"); + + /** + * Diffuse map. + */ + public var diffuseMap:TextureResource; + + /** + * Opacity map. + */ + public var opacityMap:TextureResource; + + /** + * If true, perform transparent pass. Parts of surface, cumulative alpha value of which is below than alphaThreshold draw within transparent pass. + * @see #alphaThreshold + */ + public var transparentPass:Boolean = true; + + /** + * If true, perform opaque pass. Parts of surface, cumulative alpha value of which is greater or equal than alphaThreshold draw within opaque pass. + * @see #alphaThreshold + */ + public var opaquePass:Boolean = true; + + /** + * alphaThreshold defines starts from which value of alpha a fragment of surface will get into transparent pass. + * @see #transparentPass + * @see #opaquePass + */ + public var alphaThreshold:Number = 0; + + /** + * Transparency. + */ + public var alpha:Number = 1; + + /** + * Creates a new TextureMaterial instance. + * + * @param diffuseMap Diffuse map. + * @param alpha Transparency. + */ + public function TextureMaterial(diffuseMap:TextureResource = null, opacityMap:TextureResource = null, alpha:Number = 1) { + this.diffuseMap = diffuseMap; + this.opacityMap = opacityMap; + this.alpha = alpha; + } + + /** + * @private + */ + override alternativa3d function fillResources(resources:Dictionary, resourceType:Class):void { + super.fillResources(resources, resourceType); + if (diffuseMap != null && A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(diffuseMap)) as Class, resourceType)) { + resources[diffuseMap] = true; + } + if (opacityMap != null && A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(opacityMap)) as Class, resourceType)) { + resources[opacityMap] = true; + } + } + + /** + * @param object + * @param programs + * @param camera + * @param opacityMap + * @param alphaTest 0 - disabled, 1 - opaque, 2 - contours + * @return + */ + private function getProgram(object:Object3D, programs:Vector., camera:Camera3D, opacityMap:TextureResource, alphaTest:int):ShaderProgram { + var key:int = (opacityMap != null ? 3 : 0) + alphaTest; + var program:ShaderProgram = programs[key]; + if (program == null) { + // Make program + // Vertex shader + var vertexLinker:Linker = new Linker(Context3DProgramType.VERTEX); + + var positionVar:String = "aPosition"; + vertexLinker.declareVariable(positionVar, VariableType.ATTRIBUTE); + if (object.transformProcedure != null) { + positionVar = appendPositionTransformProcedure(object.transformProcedure, vertexLinker); + } + vertexLinker.addProcedure(_projectProcedure); + vertexLinker.setInputParams(_projectProcedure, positionVar); + vertexLinker.addProcedure(_passUVProcedure); + + // Pixel shader + var fragmentLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + var outProcedure:Procedure = (opacityMap != null ? getDiffuseOpacityProcedure : getDiffuseProcedure); + fragmentLinker.addProcedure(outProcedure); + if (alphaTest > 0) { + fragmentLinker.declareVariable("tColor"); + fragmentLinker.setOutputParams(outProcedure, "tColor"); + if (alphaTest == 1) { + fragmentLinker.addProcedure(thresholdOpaqueAlphaProcedure, "tColor"); + } else { + fragmentLinker.addProcedure(thresholdTransparentAlphaProcedure, "tColor"); + } + } + fragmentLinker.varyings = vertexLinker.varyings; + + program = new ShaderProgram(vertexLinker, fragmentLinker); + + program.upload(camera.context3D); + programs[key] = program; + } + return program; + } + + private function getDrawUnit(program:ShaderProgram, camera:Camera3D, surface:Surface, geometry:Geometry, opacityMap:TextureResource):DrawUnit { + var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]); + + var object:Object3D = surface.object; + + // Draw call + var drawUnit:DrawUnit = camera.renderer.createDrawUnit(object, program.program, geometry._indexBuffer, surface.indexBegin, surface.numTriangles, program); + + // Streams + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aPosition"), positionBuffer, geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aUV"), uvBuffer, geometry._attributesOffsets[VertexAttributes.TEXCOORDS[0]], VertexAttributes.FORMATS[VertexAttributes.TEXCOORDS[0]]); + //Constants + object.setTransformConstants(drawUnit, surface, program.vertexShader, camera); + drawUnit.setProjectionConstants(camera, program.vertexShader.getVariableIndex("cProjMatrix"), object.localToCameraTransform); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cThresholdAlpha"), alphaThreshold, 0, 0, alpha); + // Textures + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sDiffuse"), diffuseMap._texture); + if (opacityMap != null) { + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sOpacity"), opacityMap._texture); + } + return drawUnit; + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, objectRenderPriority:int = -1):void { + var object:Object3D = surface.object; + + // Buffers + var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]); + + // Check validity + if (positionBuffer == null || uvBuffer == null || diffuseMap == null || diffuseMap._texture == null || opacityMap != null && opacityMap._texture == null) return; + + // Refresh program cache for this context + if (camera.context3D != cachedContext3D) { + cachedContext3D = camera.context3D; + programsCache = caches[cachedContext3D]; + if (programsCache == null) { + programsCache = new Dictionary(); + caches[cachedContext3D] = programsCache; + } + } + var optionsPrograms:Vector. = programsCache[object.transformProcedure]; + if(optionsPrograms == null) { + optionsPrograms = new Vector.(6, true); + programsCache[object.transformProcedure] = optionsPrograms; + } + + var program:ShaderProgram; + var drawUnit:DrawUnit; + // Opaque pass + if (opaquePass && alphaThreshold <= alpha) { + if (alphaThreshold > 0) { + // Alpha test + // use opacityMap if it is presented + program = getProgram(object, optionsPrograms, camera, opacityMap, 1); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap); + } else { + // do not use opacityMap at all + program = getProgram(object, optionsPrograms, camera, null, 0); + drawUnit = getDrawUnit(program, camera, surface, geometry, null); + } + // Use z-buffer within DrawCall, draws without blending + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.OPAQUE); + } + // Transparent pass + if (transparentPass && alphaThreshold > 0 && alpha > 0) { + // use opacityMap if it is presented + if (alphaThreshold <= alpha && !opaquePass) { + // Alpha threshold + program = getProgram(object, optionsPrograms, camera, opacityMap, 2); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap); + } else { + // There is no Alpha threshold or check z-buffer by previous pass + program = getProgram(object, optionsPrograms, camera, opacityMap, 0); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap); + } + // Do not use z-buffer, draws with blending + drawUnit.blendSource = Context3DBlendFactor.SOURCE_ALPHA; + drawUnit.blendDestination = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA; + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.TRANSPARENT_SORT); + } + } + + /** + * @inheritDoc + */ + override public function clone():Material { + var res:TextureMaterial = new TextureMaterial(diffuseMap, opacityMap, alpha); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Material):void { + super.clonePropertiesFrom(source); + var tex:TextureMaterial = source as TextureMaterial; + diffuseMap = tex.diffuseMap; + opacityMap = tex.opacityMap; + opaquePass = tex.opaquePass; + transparentPass = tex.transparentPass; + alphaThreshold = tex.alphaThreshold; + alpha = tex.alpha; + } + + } +} diff --git a/src/alternativa/engine3d/materials/VertexLightTextureMaterial.as b/src/alternativa/engine3d/materials/VertexLightTextureMaterial.as new file mode 100644 index 0000000..bf29db4 --- /dev/null +++ b/src/alternativa/engine3d/materials/VertexLightTextureMaterial.as @@ -0,0 +1,357 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.lights.DirectionalLight; + import alternativa.engine3d.lights.OmniLight; + import alternativa.engine3d.lights.SpotLight; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.Geometry; + import alternativa.engine3d.resources.TextureResource; + + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DProgramType; + import flash.display3D.VertexBuffer3D; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * Texture material with dynamic vertex lightning. The material is able to draw skin + * with the number of bones in surface no more than 41. To reduce the number of bones in surface can break + * the skin for more surface with fewer bones. Use the method Skin.divide(). To be drawn with this material, geometry should have UV coordinates and vertex normals​​. + * + * @see alternativa.engine3d.objects.Skin#divide() + * @see alternativa.engine3d.core.VertexAttributes#TEXCOORDS + * @see alternativa.engine3d.core.VertexAttributes#NORMAL + */ + public class VertexLightTextureMaterial extends TextureMaterial { + + private static var caches:Dictionary = new Dictionary(true); + private var cachedContext3D:Context3D; + private var programsCache:Dictionary; + + private static const _passLightingProcedure:Procedure = new Procedure(["#v0=vLightColor","mov v0, i0"], "passLightingProcedure"); + private static const _ambientLightProcedure:Procedure = new Procedure(["mov o0, i0"], "ambientLightProcedure"); + private static const _mulLightingProcedure:Procedure = new Procedure(["#v0=vLightColor","mul o0, i0, v0"], "mulLightingProcedure"); + private static const _directionalLightCode:Array = [ + "dp3 t0.x,i0,c0", + "sat t0.x,t0.x", + "mul t0, c1, t0.xxxx", + "add o0, o0, t0" + ]; + + private static const _omniLightCode:Array = [ + "sub t0, c0, i1", // L = pos - lightPos + "dp3 t0.w, t0, t0", // lenSqr + "nrm t0.xyz,t0.xyz", // L = normalize(L) + "dp3 t0.x,t0,i0", // dot = dot(normal, L) + "sqt t0.w,t0.w", // len = sqt(lensqr) + "sub t0.w, t0.w, c2.z", // len = len - atenuationBegin + "div t0.y, t0.w, c2.y", // att = len/radius + "sub t0.w, c2.x, t0.y", // att = 1 - len/radius + "sat t0.xw,t0.xw", // t = sat(t) + "mul t0.xyz,c1.xyz,t0.xxx", // t = color*t + "mul t0.xyz, t0.xyz, t0.www", + "add o0.xyz, o0.xyz, t0.xyz" + ]; + + private static const _spotLightCode:Array = [ + "sub t0, c0, i1", // L = pos - lightPos + "dp3 t0.w, t0, t0", // lenSqr + + "nrm t0.xyz,t0.xyz", // L = normalize(L) + + "dp3 t1.x, t0.xyz, c3.xyz", //axisDirDot + "dp3 t0.x,t0,i0", // dot = dot(normal, L) + + "sqt t0.w,t0.w", // len = sqt(lensqr) + "sub t0.w, t0.w, c2.y", // len = len - atenuationBegin + "div t0.y, t0.w, c2.x", // att = len/radius + "sub t0.w, c0.w, t0.y", // att = 1 - len/radius + "sub t0.y, t1.x, c2.w", + "div t0.y, t0.y, c2.z", + "sat t0.xyw,t0.xyw", // t = sat(t) + "mul t1.xyz,c1.xyz,t0.xxx", // t = color*t + "mul t1.xyz,t1.xyz,t0.yyy", // + "mul t1.xyz, t1.xyz, t0.www", + "add o0.xyz, o0.xyz, t1.xyz" + ]; + + private static const _lightsProcedures:Dictionary = new Dictionary(true); + + /** + * Creates a new VertexLightTextureMaterial instance. + * + * @param diffuse Diffuse map. + * @param alpha Transparency. + */ + public function VertexLightTextureMaterial(diffuse:TextureResource = null, opacityMap:TextureResource = null, alpha:Number = 1) { + super(diffuse, opacityMap, alpha); + } + + /** + * @inheritDoc + */ + override public function clone():Material { + var res:VertexLightTextureMaterial = new VertexLightTextureMaterial(diffuseMap, opacityMap, alpha); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @param object + * @param materialKey + * @param opacityMap + * @param alphaTest - 0:disabled 1:alpha-test 2:contours + * @param lights + * @param directionalLight + * @param lightsLength + */ + private function getProgram(object:Object3D, programs:Dictionary, camera:Camera3D, materialKey:String, opacityMap:TextureResource, alphaTest:int, lights:Vector., lightsLength:int):ShaderProgram { + var key:String = materialKey + (opacityMap != null ? "O" : "o") + alphaTest.toString(); + var program:ShaderProgram = programs[key]; + if (program == null) { + var vertexLinker:Linker = new Linker(Context3DProgramType.VERTEX); + vertexLinker.declareVariable("tTotalLight"); + vertexLinker.declareVariable("aNormal", VariableType.ATTRIBUTE); + vertexLinker.declareVariable("cAmbientColor", VariableType.CONSTANT); + vertexLinker.addProcedure(_passUVProcedure); + var positionVar:String = "aPosition"; + vertexLinker.declareVariable(positionVar, VariableType.ATTRIBUTE); + if (object.transformProcedure != null) { + positionVar = appendPositionTransformProcedure(object.transformProcedure, vertexLinker); + } + vertexLinker.addProcedure(_projectProcedure); + vertexLinker.setInputParams(_projectProcedure, positionVar); + vertexLinker.addProcedure(_ambientLightProcedure); + vertexLinker.setInputParams(_ambientLightProcedure, "cAmbientColor"); + vertexLinker.setOutputParams(_ambientLightProcedure, "tTotalLight"); + if (lightsLength > 0) { + var normalVar:String = "aNormal"; + if (object.deltaTransformProcedure != null) { + vertexLinker.declareVariable("tTransformedNormal"); + vertexLinker.addProcedure(object.deltaTransformProcedure); + vertexLinker.setInputParams(object.deltaTransformProcedure, "aNormal"); + vertexLinker.setOutputParams(object.deltaTransformProcedure, "tTransformedNormal"); + normalVar = "tTransformedNormal"; + } + for (var i:uint = 0; i < lightsLength; i++) { + var light:Light3D = lights[i]; + var lightProcedure:Procedure = _lightsProcedures[light]; + if (lightProcedure == null) { + lightProcedure = new Procedure(); + if (light is DirectionalLight) { + lightProcedure.compileFromArray(_directionalLightCode); + lightProcedure.assignVariableName(VariableType.CONSTANT, 0, "c" + light.name + "Direction"); + lightProcedure.name = "Directional" + i.toString(); + } else if (light is OmniLight) { + lightProcedure.compileFromArray(_omniLightCode); + lightProcedure.assignVariableName(VariableType.CONSTANT, 0, "c" + light.name + "Position"); + lightProcedure.assignVariableName(VariableType.CONSTANT, 2, "c" + light.name + "Radius"); + lightProcedure.name = "Omni" + i.toString(); + } else if (light is SpotLight) { + lightProcedure.compileFromArray(_spotLightCode); + lightProcedure.assignVariableName(VariableType.CONSTANT, 0, "c" + light.name + "Position"); + lightProcedure.assignVariableName(VariableType.CONSTANT, 2, "c" + light.name + "Radius"); + lightProcedure.assignVariableName(VariableType.CONSTANT, 3, "c" + light.name + "Axis"); + lightProcedure.name = "Spot" + i.toString(); + } + lightProcedure.assignVariableName(VariableType.CONSTANT, 1, "c" + light.name + "Color"); + _lightsProcedures[light] = lightProcedure; + } + vertexLinker.addProcedure(lightProcedure); + vertexLinker.setInputParams(lightProcedure, normalVar, positionVar); + vertexLinker.setOutputParams(lightProcedure, "tTotalLight"); + } + } + vertexLinker.addProcedure(_passLightingProcedure); + vertexLinker.setInputParams(_passLightingProcedure, "tTotalLight"); + + var fragmentLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + fragmentLinker.declareVariable("tColor"); + var outputProcedure:Procedure = opacityMap != null ? getDiffuseOpacityProcedure : getDiffuseProcedure; + fragmentLinker.addProcedure(outputProcedure); + fragmentLinker.setOutputParams(outputProcedure, "tColor"); + + if (alphaTest > 0) { + outputProcedure = alphaTest == 1 ? thresholdOpaqueAlphaProcedure : thresholdTransparentAlphaProcedure; + fragmentLinker.addProcedure(outputProcedure, "tColor"); + fragmentLinker.setOutputParams(outputProcedure, "tColor"); + } + fragmentLinker.addProcedure(_mulLightingProcedure, "tColor"); + + fragmentLinker.varyings = vertexLinker.varyings; + program = new ShaderProgram(vertexLinker, fragmentLinker); + + program.upload(camera.context3D); + programs[key] = program; + } + return program; + } + + private function getDrawUnit(program:ShaderProgram, camera:Camera3D, surface:Surface, geometry:Geometry, opacityMap:TextureResource, lights:Vector., lightsLength:int):DrawUnit { + // Buffers + var object:Object3D = surface.object; + + var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]); + var normalsBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.NORMAL); + + // Draw call + var drawUnit:DrawUnit = camera.renderer.createDrawUnit(object, program.program, geometry._indexBuffer, surface.indexBegin, surface.numTriangles, program); + + // Streams + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aPosition"), positionBuffer, geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aUV"), uvBuffer, geometry._attributesOffsets[VertexAttributes.TEXCOORDS[0]], VertexAttributes.FORMATS[VertexAttributes.TEXCOORDS[0]]); + + // Constants + object.setTransformConstants(drawUnit, surface, program.vertexShader, camera); + drawUnit.setProjectionConstants(camera, program.vertexShader.getVariableIndex("cProjMatrix"), object.localToCameraTransform); + drawUnit.setVertexConstantsFromVector(program.vertexShader.getVariableIndex("cAmbientColor"), camera.ambient, 1); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cThresholdAlpha"), alphaThreshold, 0, 0, alpha); + + if (lightsLength > 0) { + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aNormal"), normalsBuffer, geometry._attributesOffsets[VertexAttributes.NORMAL], VertexAttributes.FORMATS[VertexAttributes.NORMAL]); + + var i:int; + var light:Light3D; + + var transform:Transform3D; + var rScale:Number; + for (i = 0; i < lightsLength; i++) { + light = lights[i]; + transform = light.lightToObjectTransform; + var len:Number = Math.sqrt(transform.c*transform.c + transform.g*transform.g + transform.k*transform.k); + if (light is DirectionalLight) { + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("c" + light.name + "Direction"), -transform.c/len, -transform.g/len, -transform.k/len); + } else if (light is OmniLight) { + var omni:OmniLight = light as OmniLight; + rScale = Math.sqrt(transform.a*transform.a + transform.e*transform.e + transform.i*transform.i); + rScale += Math.sqrt(transform.b*transform.b + transform.f*transform.f + transform.j*transform.j); + rScale += len; + rScale /= 3; + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("c" + light.name + "Position"), transform.d, transform.h, transform.l); + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("c" + light.name + "Radius"), 1, omni.attenuationEnd*rScale - omni.attenuationBegin*rScale, omni.attenuationBegin*rScale); + } else if (light is SpotLight) { + var spot:SpotLight = light as SpotLight; + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("c" + light.name + "Position"), transform.d, transform.h, transform.l); + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("c" + light.name + "Axis"), -transform.c/len, -transform.g/len, -transform.k/len); + rScale = Math.sqrt(transform.a*transform.a + transform.e*transform.e + transform.i*transform.i); + rScale += Math.sqrt(transform.b*transform.b + transform.f*transform.f + transform.j*transform.j); + rScale += len; + rScale /= 3; + + var falloff:Number = Math.cos(spot.falloff*0.5); + var hotspot:Number = Math.cos(spot.hotspot*0.5); + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("c" + light.name + "Radius"), spot.attenuationEnd*rScale - spot.attenuationBegin*rScale, spot.attenuationBegin*rScale, hotspot == falloff ? 0.000001 : hotspot - falloff, falloff); + } + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("c" + light.name + "Color"), light.red, light.green, light.blue); + } + } + + // Textures + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sDiffuse"), diffuseMap._texture); + if (opacityMap != null) { + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sOpacity"), opacityMap._texture); + } + return drawUnit; + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, objectRenderPriority:int = -1):void { + if (diffuseMap == null || diffuseMap._texture == null || opacityMap != null && opacityMap._texture == null) return; + + var object:Object3D = surface.object; + + // Buffers + var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION); + var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]); + var normalsBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.NORMAL); + + if (positionBuffer == null || uvBuffer == null || normalsBuffer == null) return; + + // Program + var light:Light3D; + var materialKey:String = ""; + for (var i:int = 0; i < lightsLength; i++) { + light = lights[i]; + materialKey += light.lightID; + } + + // Refresh programs for this context. + if (camera.context3D != cachedContext3D) { + cachedContext3D = camera.context3D; + programsCache = caches[cachedContext3D]; + if (programsCache == null) { + programsCache = new Dictionary(); + caches[cachedContext3D] = programsCache; + } + } + + var optionsPrograms:Dictionary = programsCache[object.transformProcedure]; + if (optionsPrograms == null) { + optionsPrograms = new Dictionary(false); + programsCache[object.transformProcedure] = optionsPrograms; + } + + var program:ShaderProgram; + var drawUnit:DrawUnit; + // Opaque passOpaque pass + if (opaquePass && alphaThreshold <= alpha) { + if (alphaThreshold > 0) { + // Alpha test + // use opacityMap if it is presented + program = getProgram(object, optionsPrograms, camera, materialKey, opacityMap, 1, lights, lightsLength); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap, lights, lightsLength); + } else { + // do not use opacityMap at all + program = getProgram(object, optionsPrograms, camera, materialKey, null, 0, lights, lightsLength); + drawUnit = getDrawUnit(program, camera, surface, geometry, null, lights, lightsLength); + } + // Use z-buffer within DrawCall, draws without blending + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.OPAQUE); + } + // Transparent pass + if (transparentPass && alphaThreshold > 0 && alpha > 0) { + // use opacityMap if it is presented + if (alphaThreshold <= alpha && !opaquePass) { + // Alpha threshold + program = getProgram(object, optionsPrograms, camera, materialKey, opacityMap, 2, lights, lightsLength); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap, lights, lightsLength); + } else { + // There is no Alpha threshold or check z-buffer by previous pass + program = getProgram(object, optionsPrograms, camera, materialKey, opacityMap, 0, lights, lightsLength); + drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap, lights, lightsLength); + } + // Do not use z-buffer, draws with blending + drawUnit.blendSource = Context3DBlendFactor.SOURCE_ALPHA; + drawUnit.blendDestination = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA; + camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.TRANSPARENT_SORT); + } + } + + } +} diff --git a/src/alternativa/engine3d/materials/compiler/CommandType.as b/src/alternativa/engine3d/materials/compiler/CommandType.as new file mode 100644 index 0000000..4016aa1 --- /dev/null +++ b/src/alternativa/engine3d/materials/compiler/CommandType.as @@ -0,0 +1,59 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials.compiler { + + /** + * @private + */ + public class CommandType { + + public static const MOV:uint = 0x00; + public static const ADD:uint = 0x01; + public static const SUB:uint = 0x02; + public static const MUL:uint = 0x03; + public static const DIV:uint = 0x04; + public static const RCP:uint = 0x05; + public static const MIN:uint = 0x06; + public static const MAX:uint = 0x07; + public static const FRC:uint = 0x08; + public static const SQT:uint = 0x09; + public static const RSQ:uint = 0x0a; + public static const POW:uint = 0x0b; + public static const LOG:uint = 0x0c; + public static const EXP:uint = 0x0d; + public static const NRM:uint = 0x0e; + public static const SIN:uint = 0x0f; + public static const COS:uint = 0x10; + public static const CRS:uint = 0x11; + public static const DP3:uint = 0x12; + public static const DP4:uint = 0x13; + public static const ABS:uint = 0x14; + public static const NEG:uint = 0x15; + public static const SAT:uint = 0x16; + public static const M33:uint = 0x17; + public static const M44:uint = 0x18; + public static const M34:uint = 0x19; + public static const KIL:uint = 0x27; + public static const TEX:uint = 0x28; + public static const SGE:uint = 0x29; + public static const SLT:uint = 0x2a; + public static const SEQ:uint = 0x2c; + public static const SNE:uint = 0x2d; + public static const DEF:uint = 0x80; + public static const CAL:uint = 0x81; + public static const COMMAND_NAMES:Vector. = Vector.( + ["mov", "add", "sub", "mul", "div", "rcp", "min", "max", "frc", "sqt", "rsq", "pow", "log", "exp", + "nrm", "sin", "cos", "crs", "dp3", "dp4", "abs", "neg", "sat", "m33", "m44", "m34","1a","1b","1c","1d","1e","1f","20","21","22","23","24","25","26", "kil", "tex", "sge", "slt", "2b", "seq", "sne"] + ); + public function CommandType() { + } + } +} \ No newline at end of file diff --git a/src/alternativa/engine3d/materials/compiler/DestinationVariable.as b/src/alternativa/engine3d/materials/compiler/DestinationVariable.as new file mode 100644 index 0000000..a9f9736 --- /dev/null +++ b/src/alternativa/engine3d/materials/compiler/DestinationVariable.as @@ -0,0 +1,71 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials.compiler { + + import flash.utils.ByteArray; + + /** + * @private + */ + public class DestinationVariable extends Variable { + + public function DestinationVariable(source:String) { + var strType:String = source.match(/[tovi]/)[0]; + index = parseInt(source.match(/\d+/)[0], 10); + var swizzle:Array = source.match(/\.[xyzw]{1,4}/); + var regmask:uint; + var maskmatch:String = swizzle ? swizzle[0] : null; + if (maskmatch != null) { + regmask = 0; + var cv:int; + var maskLength:uint = maskmatch.length; + // If first char is point, then skip + for (var i:int = 1; i < maskLength; i++) { + cv = maskmatch.charCodeAt(i) - X_CHAR_CODE; + if (cv == -1) cv = 3; + regmask |= 1 << cv; + } + } else { + regmask = 0xf; // id swizzle or mask + } + lowerCode = (regmask << 16) | index; + + switch(strType){ + case "t": + lowerCode |= 0x2000000; + type = 2; + break; + case "o": + lowerCode |= 0x3000000; + type = 3; + break; + case "v": + lowerCode |= 0x4000000; + type = 4; + break; + case "i": + lowerCode |= 0x6000000; + type = 6; + break; + default : + throw new ArgumentError("Wrong destination register type, must be \"t\" or \"o\" or \"v\", var = " + source); + break; + } + } + + override public function writeToByteArray(byteCode:ByteArray, newIndex:int, newType:int, offset:int = 0):void { + byteCode.position = position + offset; + + byteCode.writeUnsignedInt((lowerCode & ~(0xf00ffff)) | newIndex | (newType << 24)); + } + + } +} diff --git a/src/alternativa/engine3d/materials/compiler/Linker.as b/src/alternativa/engine3d/materials/compiler/Linker.as new file mode 100644 index 0000000..097c54a --- /dev/null +++ b/src/alternativa/engine3d/materials/compiler/Linker.as @@ -0,0 +1,423 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials.compiler { + + import alternativa.engine3d.alternativa3d; + + import flash.display3D.Context3DProgramType; + import flash.utils.ByteArray; + import flash.utils.Dictionary; + import flash.utils.Endian; + + use namespace alternativa3d; + + /** + * @private + * Dynamic shader linker + */ + public class Linker { + + /** + * Data after linking. + */ + public var data:ByteArray = null; + + /** + * Number of used slots. + */ + public var slotsCount:int = 0; + /** + * Number of lines of the shader code. + */ + public var commandsCount:int = 0; + + /** + * Linker type. Can be vertex of fragment. + */ + public var type:String; + + private var procedures:Vector. = new Vector.(); + + /** + * @private + * Variables after linking. + */ + alternativa3d var _linkedVariables:Object; + + // Dictionary of temporary variables at this linker. Key is a name of variable, value is a variable. + private var _localVariables:Object = new Object(); + + // Key - procedure, value - array of strings. + private var _inputParams:Dictionary = new Dictionary(); + // Key - procedure, value - array of strings. + private var _outputParams:Dictionary = new Dictionary(); + + // Counters of variables by types + private var _locals:Vector. = new Vector.(6, true); + + private var samplers:Object = new Object(); + + private var _varyings:Object = new Object(); + + /** + * Creates a new Linker instance. + * + * @param programType Type of shader. + */ + public function Linker(programType:String) { + type = programType; + } + + /** + * Clears a content. + */ + public function clear():void { + data = null; + _locals[0] = _locals[1] = _locals[2] = _locals[3] = _locals[4] = _locals[5] = 0; + procedures.length = 0; + _varyings = new Object(); + samplers = new Object(); + + commandsCount = 0; + slotsCount = 0; + _linkedVariables = null; + + _inputParams = new Dictionary(); + _outputParams = new Dictionary(); + } + + /** + * Adds a new shader procedure. + * + * @param procedure Procedure to add. + * + * @see Procedure + */ + public function addProcedure(procedure:Procedure, ...args):void { + for each(var v:Variable in procedure.variablesUsages[VariableType.VARYING]) { + if (v == null) continue; + var nv:Variable = _varyings[v.name] = new Variable(); + nv.name = v.name; + nv.type = v.type; + nv.index = -1; + } + procedures.push(procedure); + _inputParams[procedure] = args; + data = null; + } + + /** + * Declaration of variable of given type. + * + * @param name Name of variable + * @param type Type of variable. Should be one of the VariableType constants. The default value is Temporary variable. + * + * @see VariableType + */ + public function declareVariable(name:String, type:uint = 2):void { + var v:Variable = new Variable(); + v.index = -1; + v.type = type; + v.name = name; + _localVariables[name] = v; + if (v.type == VariableType.VARYING) { + _varyings[v.name] = v; + } + data = null; + } + + public function declareSampler(output:String, uv:String, sampler:String, options:String):void { + if (_localVariables[uv] == null) { + throw new ArgumentError("Undefined variable " + uv); + } + + if (_localVariables[sampler] == null) { + throw new ArgumentError("Undefined variable " + sampler); + } + + if (_localVariables[output] == null) { + declareVariable(output, 2); + } + data = null; + } + + /** + * Setting of input parameters of procedure. + * + * @param procedure A procedure to which parameters will be set. + * @param args Names of variables, separated by the comma, that are passed into the procedure. + * Variables must be previously declared, using the method declareVariable(). + * + * @see #declareVariable() + */ + public function setInputParams(procedure:Procedure, ...args):void { + _inputParams[procedure] = args; + data = null; + } + + /** + * Setting of output parameters of procedure. + * + * @param procedure A procedure to which parameters will be set. + * @param args Names of variables, separated by the comma, that are passed into the procedure. + * Variables must be previously declared, using the method declareVariable(). + * + * @see #declareVariable() + */ + public function setOutputParams(procedure:Procedure, ...args):void { + _outputParams[procedure] = args; + data = null; + } + + /** + * Returns of index of variable after the linking. + * + * @param name Name of variable. + * @return Its index for sending to Context3D + */ + public function getVariableIndex(name:String):int { + if (_linkedVariables == null) throw new Error("Not linked"); + var variable:Variable = _linkedVariables[name]; + if (variable == null) { + throw new Error('Variable "' + name + '" not found'); + } + return variable.index; + } + + /** + * Returns index of variable or -1 there is no variable with such name. + */ + public function findVariable(name:String):int { + if (_linkedVariables == null) throw new Error("Has not linked"); + var variable:Variable = _linkedVariables[name]; + if (variable == null) { + return -1; + } + return variable.index; + } + + /** + * Returns the existence of this variable in linked code. + * @param name Name of variable + */ + public function containsVariable(name:String):Boolean { + if (_linkedVariables == null) throw new Error("Not linked"); + return _linkedVariables[name] != null; + } + + /** + * Linking of procedures to one shader. + */ + public function link():void { + if (data != null) return; + + var v:Variable; + var variables:Object = _linkedVariables = new Object(); + var p:Procedure; + var i:int, j:int; + var nv:Variable; + for each (v in _localVariables) { + nv = variables[v.name] = new Variable(); + nv.index = -1; + nv.type = v.type; + nv.name = v.name; + nv.size = v.size; + } + data = new ByteArray(); + data.endian = Endian.LITTLE_ENDIAN; + data.writeByte(0xa0); + data.writeUnsignedInt(0x1); // AGAL version, big endian, bit pattern will be 0x01000000 + data.writeByte(0xa1); // tag program id + data.writeByte((type == Context3DProgramType.FRAGMENT) ? 1 : 0); // vertex or fragment + + commandsCount = 0; + slotsCount = 0; + + _locals[0] = 0; + _locals[1] = 0; + _locals[2] = 0; + _locals[3] = 0; + _locals[4] = 0; + _locals[5] = 0; + // First iteration - collecting of variables. + for each (p in procedures) { + var iLength:int = p.variablesUsages.length; + _locals[1] += p.reservedConstants; + for (i = 0; i < iLength; i++) { + var vector:Vector. = p.variablesUsages[i]; + var jLength:int = vector.length; + for (j = 0; j < jLength; j++) { + v = vector[j]; + if (v == null || v.name == null) continue; + if (v.name == null && i != 2 && i != 6 && i != 3) { + throw new Error("Linkage error: Noname variable. Procedure = " + p.name + ", type = " + i.toString() + ", index = " + j.toString()); + } + nv = variables[v.name] = new Variable(); + nv.index = -1; + nv.type = v.type; + nv.name = v.name; + nv.size = v.size; + } + } + } + + for each (p in procedures) { + // Changing of inputs + var offset:int = data.length; + data.position = data.length; + data.writeBytes(p.byteCode, 0, p.byteCode.length); + var input:Array = _inputParams[p]; + var output:Array = _outputParams[p]; + var param:String; + var numParams:int; + if (input != null) { + numParams = input.length; + for (j = 0; j < numParams; j++) { + param = input[j]; + v = variables[param]; + if (v == null) { + throw new Error("Input parameter not set. paramName = " + param); + } + if (p.variablesUsages[6].length > j) { + var inParam:Variable = p.variablesUsages[6][j]; + if (inParam == null) { + throw new Error("Input parameter set, but not exist in code. paramName = " + param + ", register = i" + j.toString()); + } + if (v.index < 0) { + v.index = _locals[v.type]; + _locals[v.type] += v.size; + } + while (inParam != null) { + inParam.writeToByteArray(data, v.index, v.type, offset); + inParam = inParam.next; + } + } + + } + } + if (output != null) { + // Output parameters + numParams = output.length; + for (j = 0; j < numParams; j++) { + param = output[j]; + v = variables[param]; + if (v == null) { + if (j == 0 && (i == procedures.length - 1)) { + // Output variable + continue; + } + throw new Error("Output parameter have not declared. paramName = " + param); + } + if (v.index < 0) { + if (v.type != 2) { + throw new Error("Wrong output type:" + VariableType.TYPE_NAMES[v.type]); + } + v.index = _locals[v.type]; + _locals[v.type] += v.size; + } + var outParam:Variable = p.variablesUsages[3][j]; + if (outParam == null) { + throw new Error("Output parameter set, but not exist in code. paramName = " + param + ", register = i" + j.toString()); + } + while (outParam != null) { + outParam.writeToByteArray(data, v.index, v.type, offset); + outParam = outParam.next; + } + } + } + var vars:Vector. = p.variablesUsages[2]; + for (j = 0; j < vars.length; j++) { + v = vars[j]; + if (v == null) continue; + while (v != null) { + v.writeToByteArray(data, v.index + _locals[2], VariableType.TEMPORARY, offset); + v = v.next; + } + } + + resolveVariablesUsages(data, variables, p.variablesUsages[0], VariableType.ATTRIBUTE, offset); + resolveVariablesUsages(data, variables, p.variablesUsages[1], VariableType.CONSTANT, offset); + resolveVariablesUsages(data, _varyings, p.variablesUsages[4], VariableType.VARYING, offset); + resolveVariablesUsages(data, variables, p.variablesUsages[5], VariableType.SAMPLER, offset); + + commandsCount += p.commandsCount; + slotsCount += p.slotsCount; + } + } + private function resolveVariablesUsages(code:ByteArray, variables:Object, variableUsages:Vector., type:uint, offset:int):void { + for (var j:int = 0; j < variableUsages.length; j++) { + var vUsage:Variable = variableUsages[j]; + if (vUsage == null) continue; + if (vUsage.isRelative) continue; + var variable:Variable = variables[vUsage.name]; + if (variable.index < 0) { + variable.index = _locals[type]; + _locals[type] += variable.size; + } + while (vUsage != null) { + vUsage.writeToByteArray(code, variable.index, variable.type, offset); + vUsage = vUsage.next; + } + } + } + + /** + * Returns description of procedures: name, size, input and output parameters. + * @return + */ + public function describeLinkageInfo():String { + var str:String; + var result:String = "LINKER:\n"; + var totalCodes:uint = 0; + var totalCommands:uint = 0; + for (var i:int = 0; i < procedures.length; i++) { + var p:Procedure = procedures[i]; + if (p.name != null) { + result += p.name + "("; + } else { + result += "#" + i.toString() + "("; + } + var args:* = _inputParams[p]; + if (args != null) { + for each (str in args) { + result += str + ","; + } + result = result.substr(0, result.length - 1); + } + result += ")"; + args = _outputParams[p]; + if (args != null) { + result += "->("; + for each (str in args) { + result += str + ","; + } + result = result.substr(0, result.length - 1); + result += ")"; + } + result += " [IS:" + p.slotsCount.toString() + ", CMDS:" + p.commandsCount.toString() + "]\n"; + totalCodes += p.slotsCount; + totalCommands += p.commandsCount; + } + result += "[IS:" + totalCodes.toString() + ", CMDS:" + totalCommands.toString() + "]\n"; + return result; + } + + public function get varyings():Object { + return _varyings; + } + + public function set varyings(value:Object):void { + _varyings = value; + data = null; + } + + } +} diff --git a/src/alternativa/engine3d/materials/compiler/Procedure.as b/src/alternativa/engine3d/materials/compiler/Procedure.as new file mode 100644 index 0000000..063727e --- /dev/null +++ b/src/alternativa/engine3d/materials/compiler/Procedure.as @@ -0,0 +1,487 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials.compiler { + + import alternativa.engine3d.alternativa3d; + + import flash.display3D.Context3DProgramType; + + import flash.utils.ByteArray; + import flash.utils.Endian; + + use namespace alternativa3d; + + /** + * @private + * Shader procedure + */ + public class Procedure { + + // Name of procedure + public var name:String; + + alternativa3d static const crc32Table:Vector. = createCRC32Table(); + + private static function createCRC32Table():Vector. { + var crc_table:Vector. = new Vector.(256); + var crc:uint, i:int, j:int; + for (i = 0; i < 256; i++) { + crc = i; + for (j = 0; j < 8; j++) + crc = crc & 1 ? (crc >> 1) ^ 0xEDB88320 : crc >> 1; + + crc_table[i] = crc; + } + return crc_table; + } + + alternativa3d var crc32:uint = 0; + + /** + * Code of procedure. + */ + public var byteCode:ByteArray = new ByteArray(); + public var variablesUsages:Vector.> = new Vector.>(); + + /** + * Number of instruction slots in a procedure. + */ + public var slotsCount:int = 0; + + /** + * Number of strings in a procedure. + */ + public var commandsCount:int = 0; + + alternativa3d var reservedConstants:uint = 0; + + /** + * Creates a new Procedure instance. + * + * @param array Array of AGAL strings + */ + public function Procedure(array:Array = null, name:String = null) { + byteCode.endian = Endian.LITTLE_ENDIAN; + this.name = name; + if (array != null) { + compileFromArray(array); + } + } + + public function getByteCode(type:String):ByteArray { + var result:ByteArray = new ByteArray(); + result.endian = Endian.LITTLE_ENDIAN; + result.writeByte(0xa0); + result.writeUnsignedInt(0x1); // AGAL version, big endian, bit pattern will be 0x01000000 + result.writeByte(0xa1); // tag program id + result.writeByte((type == Context3DProgramType.FRAGMENT) ? 1 : 0); // vertex or fragment + result.writeBytes(byteCode); + return result; + } + + private function addVariableUsage(v:Variable):void { + var vars:Vector. = variablesUsages[v.type]; + var index:int = v.index; + if (index >= vars.length) { + vars.length = index + 1; + } else { + v.next = vars[index]; + } + vars[index] = v; + } + + /** + * Sets name and size of variable + * + * @param type Type of variable. One of VariableType constants. + * @param index Index of variable at shader code. + * @param name Assigned variable name. + * @param size Size of variable in vectors. + * + * @see VariableType + */ + public function assignVariableName(type:uint, index:uint, name:String, size:uint = 1):void { + var v:Variable = variablesUsages[type][index]; + while (v != null) { + v.size = size; + v.name = name; + v = v.next; + } + } + + /** + * Compiles shader from the string. + */ + public function compileFromString(source:String):void { + var commands:Array = source.split("\n"); + compileFromArray(commands); + } + + /** + * Compiles shader from the array of strings. + */ + public function compileFromArray(source:Array):void { + for (var i:int = 0; i < 7; i++) { + variablesUsages[i] = new Vector.(); + } + byteCode.length = 0; + commandsCount = 0; + slotsCount = 0; + + var declarationStrings:Vector. = new Vector.(); + var count:int = source.length; + for (i = 0; i < count; i++) { + var cmd:String = source[i]; + var declaration:Array = cmd.match(/# *[acvs]\d{1,3} *= *[a-zA-Z0-9_]*/i); + if (declaration != null && declaration.length > 0) { + declarationStrings.push(declaration[0]); + } else { + writeCommand(cmd); + } + } + for (i = 0,count = declarationStrings.length; i < count; i++) { + var decArray:Array = declarationStrings[i].split("="); + var regType:String = decArray[0].match(/[acvs]/i); + var varIndex:int = int(decArray[0].match(/\d{1,3}/i)); + var varName:String = decArray[1].match(/[a-zA-Z0-9]*/i); + switch (regType.toLowerCase()) { + case "a": + assignVariableName(VariableType.ATTRIBUTE, varIndex, varName); + break; + case "c": + assignVariableName(VariableType.CONSTANT, varIndex, varName); + break; + case "v": + assignVariableName(VariableType.VARYING, varIndex, varName); + break; + case "s": + assignVariableName(VariableType.SAMPLER, varIndex, varName); + break; + } + } + crc32 = createCRC32(byteCode); + } + + public function assignConstantsArray(registersCount:uint = 1):void { + reservedConstants = registersCount; + } + + private function writeCommand(source:String):void { + var commentIndex:int = source.indexOf("//"); + if (commentIndex >= 0) { + source = source.substr(0, commentIndex); + } + // mov vt0, v0 + // mov vt0, v0, vc1 + // mov vt0.xy, a0.xy, vc1.xy + // mov vt0.xy, a0.xy, vc1.xy + // mov vt0.xy, v0[va1.x + 2], vc[va0.x + 2] + // mov op, v0[va1.x + 2], vc[va0.x + 2] + // tex t0, v0, s0 <2d, linear> + + // Errors: + //1) Merged commands + //2) Syntax errors + //-- incorrect number of operands + //-- unknown commands + //-- unknown registers + //-- unknown constructions + //3) Using of unwritable registers + //-- in vertex shader (va0, c0, s0); + //-- in fragment shader (v0, va0, c0, s0); + //4) Using of unreadable registers + //-- in vertex shader (v0, s0); + //-- in fragment shader (va0); + //5) Deny write into the input registers + //6) Mismatch the size of types of registers + //7) Relative addressing in the fragment shader is not possible + //-- You can not use it for recording + //-- Offset is out of range [0..255] + //8) Flow errors + //-- unused variable + //-- using of uninitialized variable + //-- using of partially uninitialized variable + //-- function is not return value + //9) Restrictions + //-- too many commands + //-- too many constants + //-- too many textures + //-- too many temporary variables + //-- too many interpolated values + // You can not use kil in fragment shader + + var operands:Array = source.match(/[A-Za-z]+(((\[.+\])|(\d+))(\.[xyzw]{1,4})?(\ *\<.*>)?)?/g); + + // It is possible not use the input parameter. It is optimization of the linker + // Determine the size of constant + + if (operands.length < 2) { + return; + } + var opCode:String = operands[0]; + var destination:Variable; + var source1:SourceVariable; + var source2:Variable; + if (opCode == "kil") { + source1 = new SourceVariable(operands[1]); + } else { + destination = new DestinationVariable(operands[1]); + source1 = new SourceVariable(operands[2]); + addVariableUsage(destination); + } + addVariableUsage(source1); + + var type:uint; + switch (opCode) { + case "mov": + type = CommandType.MOV; + slotsCount++; + break; + case "add": + type = CommandType.ADD; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "sub": + type = CommandType.SUB; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "mul": + type = CommandType.MUL; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "div": + type = CommandType.DIV; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "rcp": + type = CommandType.RCP; + slotsCount++; + break; + case "min": + type = CommandType.MIN; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "max": + type = CommandType.MAX; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "frc": + type = CommandType.FRC; + slotsCount++; + break; + case "sqt": + type = CommandType.SQT; + slotsCount++; + break; + case "rsq": + type = CommandType.RSQ; + slotsCount++; + break; + case "pow": + type = CommandType.POW; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount += 3; + break; + case "log": + type = CommandType.LOG; + slotsCount++; + break; + case "exp": + type = CommandType.EXP; + slotsCount++; + break; + case "nrm": + type = CommandType.NRM; + slotsCount += 3; + break; + case "sin": + type = CommandType.SIN; + slotsCount += 8; + break; + case "cos": + type = CommandType.COS; + slotsCount += 8; + break; + case "crs": + type = CommandType.CRS; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount += 2; + break; + case "dp3": + type = CommandType.DP3; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "dp4": + type = CommandType.DP4; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "abs": + type = CommandType.ABS; + slotsCount++; + break; + case "neg": + type = CommandType.NEG; + slotsCount++; + break; + case "sat": + type = CommandType.SAT; + slotsCount++; + break; + case "m33": + type = CommandType.M33; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount += 3; + break; + case "m44": + type = CommandType.M44; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount += 4; + break; + case "m34": + type = CommandType.M34; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount += 3; + break; + case "kil": + type = CommandType.KIL; + slotsCount++; + break; + case "tex": + type = CommandType.TEX; + source2 = new SamplerVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "sge": + type = CommandType.SGE; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "slt": + type = CommandType.SLT; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "seq": + type = CommandType.SEQ; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + case "sne": + type = CommandType.SNE; + source2 = new SourceVariable(operands[3]); + addVariableUsage(source2); + slotsCount++; + break; + default: + break; + } + // Fill of byteCode of command + byteCode.writeUnsignedInt(type); + if (destination != null) { + destination.position = byteCode.position; + byteCode.writeUnsignedInt(destination.lowerCode); + } else { + byteCode.writeUnsignedInt(0); + } + source1.position = byteCode.position; + if (source1.relative != null) { + addVariableUsage(source1.relative); + source1.relative.position = byteCode.position; + } + byteCode.writeUnsignedInt(source1.lowerCode); + byteCode.writeUnsignedInt(source1.upperCode); + if (source2 != null) { + var s2v:SourceVariable = source2 as SourceVariable; + source2.position = byteCode.position; + if (s2v != null && s2v.relative != null) { + addVariableUsage(s2v.relative); + s2v.relative.position = s2v.position; + } + byteCode.writeUnsignedInt(source2.lowerCode); + byteCode.writeUnsignedInt(source2.upperCode); + } else { + byteCode.position = (byteCode.length += 8); + } + commandsCount++; + } + + /** + * Creates and returns an instance of procedure from array of strings. + */ + public static function compileFromArray(source:Array, name:String = null):Procedure { + var proc:Procedure = new Procedure(source, name); + return proc; + } + + /** + * Creates and returns an instance of procedure from string. + */ + public static function compileFromString(source:String, name:String = null):Procedure { + var proc:Procedure = new Procedure(null, name); + proc.compileFromString(source); + return proc; + } + + /** + * Create an instance of procedure. + */ + public function newInstance():Procedure { + var res:Procedure = new Procedure(); + res.byteCode = this.byteCode; + res.variablesUsages = this.variablesUsages; + res.slotsCount = this.slotsCount; + res.reservedConstants = this.reservedConstants; + res.commandsCount = this.commandsCount; + res.name = name; + return res; + } + + + + + alternativa3d static function createCRC32(byteCode:ByteArray):uint { + byteCode.position = 0; + var len:uint = byteCode.length; + var crc:uint = 0xFFFFFFFF; + while (len--) { + var byte:int = byteCode.readByte(); + crc = crc32Table[(crc ^ byte) & 0xFF] ^ (crc >> 8); + } + return crc ^ 0xFFFFFFFF; + } + } + +} diff --git a/src/alternativa/engine3d/materials/compiler/RelativeVariable.as b/src/alternativa/engine3d/materials/compiler/RelativeVariable.as new file mode 100644 index 0000000..78cb232 --- /dev/null +++ b/src/alternativa/engine3d/materials/compiler/RelativeVariable.as @@ -0,0 +1,66 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials.compiler +{ + import flash.utils.ByteArray; + + /** + * @private + */ + public class RelativeVariable extends Variable { + + public function RelativeVariable(source:String) { + var relname:Array = source.match( /[A-Za-z]/g ); + index = parseInt(source.match(/\d+/g)[0], 10); + switch(relname[0]){ + case "a": + type = VariableType.ATTRIBUTE; + break; + case "c": + type = VariableType.CONSTANT; + break; + case "t": + type = VariableType.TEMPORARY; + break; + case "i": + type = VariableType.INPUT; + break; + } + var selmatch:Array = source.match(/(\.[xyzw]{1,1})/); + if (selmatch.length == 0) { + throw new Error("error: bad index register select"); + } + var relsel:int = selmatch[0].charCodeAt(1) - X_CHAR_CODE; + if (relsel == -1) relsel = 3; + var relofs:Array = source.match(/\+\d{1,3}/g); + var reloffset:int = 0; + if (relofs.length > 0) { + reloffset = parseInt(relofs[0], 10); + } + if (reloffset < 0 || reloffset > 255) { + throw new Error("Error: index offset " + reloffset + " out of bounds. [0..255]"); + } + + lowerCode = reloffset << 16 | index; + upperCode |= type << 8; + upperCode |= relsel << 16; + upperCode |= 1 << 31; + } + + override public function writeToByteArray(byteCode:ByteArray, newIndex:int, newType:int, offset:int = 0):void { + byteCode.position = position + offset; + byteCode.writeShort(newIndex); + byteCode.position = position + offset + 5; + byteCode.writeByte(newType); + } + + } +} diff --git a/src/alternativa/engine3d/materials/compiler/SamplerVariable.as b/src/alternativa/engine3d/materials/compiler/SamplerVariable.as new file mode 100644 index 0000000..2ea3dca --- /dev/null +++ b/src/alternativa/engine3d/materials/compiler/SamplerVariable.as @@ -0,0 +1,99 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials.compiler { + + import flash.utils.ByteArray; + + /** + * @private + */ + public class SamplerVariable extends Variable { + + public function SamplerVariable(source:String) { + var strType:String = String(source.match(/[si]/g)[0]); + switch(strType){ + case "s": + upperCode = VariableType.SAMPLER; + break; + case "i": + upperCode = VariableType.INPUT; + break; + } + index = parseInt(source.match(/\d+/g)[0], 10); + lowerCode = index; + var optsi:int = source.search(/<.*>/g); + var opts:Array; + if (optsi != -1) { + opts = source.substring(optsi).match(/(\w+)/g); + } + type = upperCode; + //upperInt = 5; // type 5 + var optsLength:uint = opts.length; + for (var i:int = 0; i < optsLength; i++) { + var op:String = opts[i]; + switch(op){ + case "2d": + upperCode &= ~(0xf000); + break; + case "3d": + upperCode &= ~(0xf000); + upperCode |= 0x2000; + break; + case "cube": + upperCode &= ~(0xf000); + upperCode |= 0x1000; + break; + case "mipnearest": + upperCode &= ~(0xf000000); + upperCode |= 0x1000000; + break; + case "miplinear": + upperCode &= ~(0xf000000); + upperCode |= 0x2000000; + break; + case "mipnone": + case "nomip": + upperCode &= ~(0xf000000); + break; + case "nearest": + upperCode &= ~(0xf0000000); + break; + case "linear": + upperCode &= ~(0xf0000000); + upperCode |= 0x10000000; + break; + case "centroid": + upperCode |= 0x100000000; + break; + case "single": + upperCode |= 0x200000000; + break; + case "depth": + upperCode |= 0x400000000; + break; + case "repeat": + case "wrap": + upperCode &= ~(0xf00000); + upperCode |= 0x100000; + break; + case "clamp": + upperCode &= ~(0xf00000); + break; + } + } + } + + override public function writeToByteArray(byteCode:ByteArray, newIndex:int, newType:int, offset:int = 0):void { + super.writeToByteArray(byteCode, newIndex, newType, offset); + } + + } +} diff --git a/src/alternativa/engine3d/materials/compiler/SourceVariable.as b/src/alternativa/engine3d/materials/compiler/SourceVariable.as new file mode 100644 index 0000000..d144ded --- /dev/null +++ b/src/alternativa/engine3d/materials/compiler/SourceVariable.as @@ -0,0 +1,104 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials.compiler { + + import flash.utils.ByteArray; + + /** + * @private + */ + public class SourceVariable extends Variable { + + public var relative:RelativeVariable; + + override public function get size():uint { + if(relative){ + return 0; + } + return super.size; + } + + public function SourceVariable(source:String) { + var strType:String = String(source.match(/[catsoiv]/g)[0]); + + var regmask:uint; + + var relreg:Array = source.match( /\[.*\]/g ); + var isRel:Boolean = relreg.length > 0; + if(isRel){ + source = source.replace(relreg[0], "0"); + } else { + index = parseInt(source.match(/\d+/g)[0], 10); + } + + var swizzle:Array = source.match(/\.[xyzw]{1,4}/); + + var maskmatch:String = swizzle ? swizzle[0] : null; + if (maskmatch) { + regmask = 0; + var cv:int; + var maskLength:uint = maskmatch.length; + for (var i:int = 1; i < maskLength; i++) { + cv = maskmatch.charCodeAt(i) - X_CHAR_CODE; + if (cv == -1) cv = 3; + regmask |= cv << ( ( i - 1 ) << 1 ); + } + for ( ; i <= 4; i++ ) + regmask |= cv << ( ( i - 1 ) << 1 ); // repeat last + } else { + regmask = 0xe4; // id swizzle or mask + } + lowerCode = (regmask << 24) | index; + + switch(strType){ + case "a": + type = VariableType.ATTRIBUTE; + break; + case "c": + type = VariableType.CONSTANT; + break; + case "t": + type = VariableType.TEMPORARY; + break; + case "o": + type = VariableType.OUTPUT; + break; + case "v": + type = VariableType.VARYING; + break; + case "i": + type = VariableType.INPUT; + break; + default : + throw new ArgumentError('Wrong source register type, must be "a" or "c" or "t" or "o" or "v" or "i", var = ' + source); + break; + } + upperCode = type; + if (isRel) { + relative = new RelativeVariable(relreg[0]); + lowerCode |= relative.lowerCode; + upperCode |= relative.upperCode; + isRelative = true; + } + } + + override public function writeToByteArray(byteCode:ByteArray, newIndex:int, newType:int, offset:int = 0):void { + if (relative == null) { + super.writeToByteArray(byteCode, newIndex, newType, offset); + } else { + byteCode.position = position + 2; + } + byteCode.position = position + offset + 4; + byteCode.writeByte(newType); + } + + } +} diff --git a/src/alternativa/engine3d/materials/compiler/Variable.as b/src/alternativa/engine3d/materials/compiler/Variable.as new file mode 100644 index 0000000..eb6c3a6 --- /dev/null +++ b/src/alternativa/engine3d/materials/compiler/Variable.as @@ -0,0 +1,71 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials.compiler { + + import flash.utils.ByteArray; + + /** + * @private + */ + public class Variable { + + protected static const X_CHAR_CODE:Number = "x".charCodeAt(0); + + public var name:String; + // Index of register. + public var index:int; + // Type of VariableType register. + public var type:uint; + // Location of calling for variable in byte code. + public var position:uint = 0; + // Next calling for variable with the same index. + public var next:Variable; + + public var lowerCode:uint; + public var upperCode:uint; + public var isRelative:Boolean; + private var _size:uint = 1; + + private static var collector:Variable; + + public static function create():Variable { + if(collector == null){ + collector = new Variable(); + } + var output:Variable = collector; + collector = collector.next; + output.next = null; + return output; + } + + public function dispose():void { + next = collector; + collector = this; + } + + public function Variable() { + } + + public function get size():uint { + return _size; + } + + public function set size(value:uint):void { + _size = value; + } + + public function writeToByteArray(byteCode:ByteArray, newIndex:int, newType:int, offset:int = 0):void { + byteCode.position = position + offset; + byteCode.writeShort(newIndex); + } + + } +} diff --git a/src/alternativa/engine3d/materials/compiler/VariableType.as b/src/alternativa/engine3d/materials/compiler/VariableType.as new file mode 100644 index 0000000..43e25c8 --- /dev/null +++ b/src/alternativa/engine3d/materials/compiler/VariableType.as @@ -0,0 +1,55 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.materials.compiler { + + /** + * @private + * Types of shader variables + */ + public class VariableType { + + /** + * Input attribute of vertex. + */ + public static const ATTRIBUTE:uint = 0; + /** + * Constant. + */ + public static const CONSTANT:uint = 1; + /** + * Temporary variable. + */ + public static const TEMPORARY:uint = 2; + /** + * Output variable. + */ + public static const OUTPUT:uint = 3; + /** + * Interpolated variable. + */ + public static const VARYING:uint = 4; + /** + * Texture. + */ + public static const SAMPLER:uint = 5; + /** + * Input variable. + */ + public static const INPUT:uint = 6; + + public static const TYPE_NAMES:Vector. = Vector.( + ["attribute", "constant", "temporary", "output", "varying", "sampler", "input"] + ); + public function VariableType() { + } + + } +} diff --git a/src/alternativa/engine3d/objects/AnimSprite.as b/src/alternativa/engine3d/objects/AnimSprite.as new file mode 100644 index 0000000..e2f3b8b --- /dev/null +++ b/src/alternativa/engine3d/objects/AnimSprite.as @@ -0,0 +1,144 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.materials.Material; + + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * Animated sprite. Instances of Material use as frames for animation. Playing the animation can be done through changing frame property. + * I. e. + * animSprite.frame++;
+ * camera.render(context3D)
+ */ + public class AnimSprite extends Sprite3D { + private var _materials:Vector.; + private var _frame:int = 0; + private var _loop:Boolean = false; + + /** + * Creates a new AnimSprite instance. + * @param width Width. + * @param height Height. + * @param materials List of materials. + * @param loop If true, Loops animation. + * @param frame Current frame. + * @see alternativa.engine3d.materials.Material + */ + public function AnimSprite(width:Number, height:Number, materials:Vector. = null, loop:Boolean = false, frame:int = 0) { + super(width, height); + _materials = materials; + _loop = loop; + this.frame = frame; + } + + /** + * List of materials. + */ + public function get materials():Vector. { + return _materials; + } + + /** + * @private + */ + public function set materials(value:Vector.):void { + _materials = value; + if (value != null) { + frame = _frame; + } else { + material = null; + } + } + + /** + * In case of true, when frame takes value greater than length of materials list, it switches to begin. + * Otherwise the value of frame property will equal materials.length-1 after setting greater value. + * @see #frame + * @see #materials + */ + public function get loop():Boolean { + return _loop; + } + + /** + * @private + */ + public function set loop(value:Boolean):void { + _loop = value; + frame = _frame; + } + + /** + * Current frame of animation. While rendering, the material to draw AnimSprite will taken from materials list according to value of this property. + * @see #loop + * @see #materials + */ + public function get frame():int { + return _frame; + } + + /** + * @private + */ + public function set frame(value:int):void { + _frame = value; + if (_materials != null) { + var materialsLength:int = _materials.length; + var index:int = _frame; + if (_frame < 0) { + var mod:int = _frame%materialsLength; + index = (_loop && mod != 0) ? (mod + materialsLength) : 0; + } else if (_frame > materialsLength - 1) { + index = _loop ? (_frame%materialsLength) : (materialsLength - 1); + } + material = _materials[index]; + } + } + + /** + * @private + */ + alternativa3d override function fillResources(resources:Dictionary, hierarchy:Boolean = false, resourceType:Class = null):void { + if (materials != null) { + for each (var material:Material in materials) { + if (material != null) material.fillResources(resources, resourceType); + } + } + super.fillResources(resources, hierarchy, resourceType); + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:AnimSprite = new AnimSprite(width, height); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + var src:AnimSprite = source as AnimSprite; + _materials = src._materials; + _loop = src._loop; + _frame = src._frame; + } + } +} diff --git a/src/alternativa/engine3d/objects/AxisAlignedSprite.as b/src/alternativa/engine3d/objects/AxisAlignedSprite.as new file mode 100644 index 0000000..765b6bb --- /dev/null +++ b/src/alternativa/engine3d/objects/AxisAlignedSprite.as @@ -0,0 +1,273 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.resources.Geometry; + + import flash.display3D.Context3D; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * AxisAlignedSprite is a flat image which keeps vertical orientation but able to revolve on its own z-axis. Z-rotation defines by relative position of a sprite and a camera. + * AxisAlignedSprite can look to the camera as well as keep the same direction with it. In the first case normal of the sprite point to the camera, + * and normal is parallel with z-axis of the camera in second case. Set alignToView to true for the second case and false otherwise. + * Please note, if z-axis of the AxisAlignedSprite point to the camera, you will not able see it. + */ + public class AxisAlignedSprite extends Object3D { + + static private const geometries:Dictionary = new Dictionary(); + + static private var transformProcedureStatic:Procedure = new Procedure([ + // Pivot + "sub t0.x, i0.x, c0.x", + "add t0.z, i0.z, c0.y", + // Width and height + "mul t0.x, t0.x, c0.z", + "mul o0.z, t0.z, c0.w", + // Rotation + "mov t1.z, c1.x", + "sin t1.x, t1.z", // sin + "cos t1.y, t1.z", // cos + "mul o0.x, t0.x, t1.y", // x*cos + "mul o0.y, t0.x, t1.x", // x*sin + "mov o0.w, i0.w", + // Declaration + "#c0=size", // originX, originY, width, height + "#c1=rotation", // angle, 0, 0, 1 + ]); + + static private var deltaTransformProcedureStatic:Procedure = new Procedure([ + // Rotation + "mov t1.z, c1.x", + "sin t1.x, t1.z", // sin + "neg t1.x, t1.x", + "cos t1.y, t1.z", // cos + "mul o0.x, i0.y, t1.x", // y*sin + "mul o0.y, i0.y, t1.y", // y*cos + "mov o0.z, i0.z", + "mov o0.w, i0.w", + // Declaration + "#c0=size", // originX, originY, width, height + "#c1=rotation", // angle, 0, 0, 1 + ]); + + /** + * Horizontal coordinate in the AxisAlignedSprite plane which defines what part of the plane will placed in x = 0 of the AxisAlignedSprite object. The dimension considered with UV-coordinates. + * Thus, if originX = 0, image will drawn from 0 to the right, if originX = -1 – to the left. + * And image will drawn in the center of the AxisAlignedSprite, if originX = 0.5. + */ + public var originX:Number = 0.5; + + /** + * Vertical coordinate in the AxisAlignedSprite plane which defines what part of the plane will placed in y = 0 of the AxisAlignedSprite object. The dimension considered with UV-coordinates. + * Thus, if originY = 0, image will drawn from 0 to the bottom, if originY = -1 – to the top. + * And image will drawn in the center of the AxisAlignedSprite, if originY = 0.5. + */ + public var originY:Number = 0.5; + + /** + * Width + */ + public var width:Number; + + /** + * Height + */ + public var height:Number; + + /** + * If true, the normal of the AxisAlignedSprite will be parallel to z-axis of the camera, otherwise the normal will point to the camera. + */ + public var alignToView:Boolean = true; + + /** + * @private + */ + alternativa3d var surface:Surface; + + /** + * Creates a new AxisAlignedSprite instance. + * @param width Width + * @param height Height + * @param material The material. + * @see alternativa.engine3d.materials.Material + */ + public function AxisAlignedSprite(width:Number, height:Number, material:Material = null) { + this.width = width; + this.height = height; + surface = new Surface(); + surface.object = this; + this.material = material; + surface.indexBegin = 0; + surface.numTriangles = 2; + // Transform position to the local space + transformProcedure = transformProcedureStatic; + // Transform vector to the local space + deltaTransformProcedure = deltaTransformProcedureStatic; + } + + /** + * Material of a sprite. + * @see alternativa.engine3d.materials.Material + */ + public function get material():Material { + return surface.material; + } + + /** + * @private + */ + public function set material(value:Material):void { + surface.material = value; + } + + /** + * @private + */ + alternativa3d override function fillResources(resources:Dictionary, hierarchy:Boolean = false, resourceType:Class = null):void { + if (surface.material != null) surface.material.fillResources(resources, resourceType); + super.fillResources(resources, hierarchy, resourceType); + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + var geometry:Geometry = getGeometry(camera.context3D); + if (surface.material != null) surface.material.collectDraws(camera, surface, geometry, lights, lightsLength); + // Mouse events + if (listening) camera.view.addSurfaceToMouseEvents(surface, geometry, transformProcedure); + } + + /** + * @private + */ + override alternativa3d function setTransformConstants(drawUnit:DrawUnit, surface:Surface, vertexShader:Linker, camera:Camera3D):void { + // Set constants + drawUnit.setVertexConstantsFromNumbers(0, originX, originY, width, height); + if (alignToView || camera.orthographic) { + drawUnit.setVertexConstantsFromNumbers(1, Math.PI - Math.atan2(-cameraToLocalTransform.c, -cameraToLocalTransform.g), 0, 0, 1); + } else { + drawUnit.setVertexConstantsFromNumbers(1, Math.PI - Math.atan2(cameraToLocalTransform.d, cameraToLocalTransform.h), 0, 0, 1); + } + } + + /** + * @private + */ + alternativa3d function getGeometry(context:Context3D):Geometry { + var geometry:Geometry = geometries[context]; + if (geometry == null) { + geometry = new Geometry(4); + + var attributes:Array = new Array(); + attributes[0] = VertexAttributes.POSITION; + attributes[1] = VertexAttributes.POSITION; + attributes[2] = VertexAttributes.POSITION; + attributes[3] = VertexAttributes.NORMAL; + attributes[4] = VertexAttributes.NORMAL; + attributes[5] = VertexAttributes.NORMAL; + attributes[6] = VertexAttributes.TEXCOORDS[0]; + attributes[7] = VertexAttributes.TEXCOORDS[0]; + attributes[8] = VertexAttributes.TEXCOORDS[1]; + attributes[9] = VertexAttributes.TEXCOORDS[1]; + attributes[10] = VertexAttributes.TEXCOORDS[2]; + attributes[11] = VertexAttributes.TEXCOORDS[2]; + attributes[12] = VertexAttributes.TEXCOORDS[3]; + attributes[13] = VertexAttributes.TEXCOORDS[3]; + attributes[14] = VertexAttributes.TEXCOORDS[4]; + attributes[15] = VertexAttributes.TEXCOORDS[4]; + attributes[16] = VertexAttributes.TEXCOORDS[5]; + attributes[17] = VertexAttributes.TEXCOORDS[5]; + attributes[18] = VertexAttributes.TEXCOORDS[6]; + attributes[19] = VertexAttributes.TEXCOORDS[6]; + attributes[20] = VertexAttributes.TEXCOORDS[7]; + attributes[21] = VertexAttributes.TEXCOORDS[7]; + attributes[22] = VertexAttributes.TANGENT4; + attributes[23] = VertexAttributes.TANGENT4; + attributes[24] = VertexAttributes.TANGENT4; + attributes[25] = VertexAttributes.TANGENT4; + geometry.addVertexStream(attributes); + + geometry.setAttributeValues(VertexAttributes.POSITION, Vector.([0, 0, 0, 0, 0, -1, 1, 0, -1, 1, 0, 0])); + geometry.setAttributeValues(VertexAttributes.NORMAL, Vector.([0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[0], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[1], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[2], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[3], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[4], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[5], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[6], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[7], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + + geometry.indices = Vector.([0, 1, 3, 2, 3, 1]); + + geometry.upload(context); + geometries[context] = geometry; + } + return geometry; + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:AxisAlignedSprite = new AxisAlignedSprite(width, height); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + var src:AxisAlignedSprite = source as AxisAlignedSprite; + width = src.width; + height = src.height; + material = src.material; + originX = src.originX; + originY = src.originY; + alignToView = src.alignToView; + } + + /** + * @private + */ + override alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + if (transform != null) { + // TODO: + } + var radius:Number = ((originX >= 0.5) ? originX : (1 - originX))*width; + var top:Number = originY*height; + var bottom:Number = (originY - 1)*height; + if (-radius < boundBox.minX) boundBox.minX = -radius; + if (radius > boundBox.maxX) boundBox.maxX = radius; + if (-radius < boundBox.minY) boundBox.minY = -radius; + if (radius > boundBox.maxY) boundBox.maxY = radius; + if (bottom < boundBox.minZ) boundBox.minZ = bottom; + if (top > boundBox.maxZ) boundBox.maxZ = top; + } + } +} diff --git a/src/alternativa/engine3d/objects/Decal.as b/src/alternativa/engine3d/objects/Decal.as new file mode 100644 index 0000000..03ae83b --- /dev/null +++ b/src/alternativa/engine3d/objects/Decal.as @@ -0,0 +1,110 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + + use namespace alternativa3d; + + /** + * A Mesh which has z-fighting engine. Most popular case of use is for dynamic addition of different tracks over existing surfaces. + * The Plane instance can be used as the geometry source. + * + * var plane = new Plane(200, 200); + * var decal = new Decal(); + * decal.geometry = plane.geometry; + * for (var i:int = 0; i < plane.numSurfaces; i++){ + * decal.addSurface(null, plane.getSurface(i).indexBegin, plane.getSurface(i).numTriangles);} + * decal.geometry.upload(stage3D.context3D); + * + */ + public class Decal extends Mesh { + + static private var transformProcedureStatic:Procedure = new Procedure([ + // Z in a camera + "dp4 t0.z, i0, c0", + // delta calculates with z*z/(zNear*(1<, lightsLength:int):void { + for (var i:int = 0; i < _surfacesLength; i++) { + var surface:Surface = _surfaces[i]; + if (surface.material != null) surface.material.collectDraws(camera, surface, geometry, lights, lightsLength, Renderer.DECALS); + // Mouse events + if (listening) camera.view.addSurfaceToMouseEvents(surface, geometry, transformProcedure); + } + } + + /** + * @private + */ + override alternativa3d function setTransformConstants(drawUnit:DrawUnit, surface:Surface, vertexShader:Linker, camera:Camera3D):void { + drawUnit.setVertexConstantsFromNumbers(vertexShader.getVariableIndex("cCam"), cameraToLocalTransform.d, cameraToLocalTransform.h, cameraToLocalTransform.l, camera.nearClipping*(1 << zBufferPrecision)); + drawUnit.setVertexConstantsFromNumbers(vertexShader.getVariableIndex("cTrm"), localToCameraTransform.i, localToCameraTransform.j, localToCameraTransform.k, localToCameraTransform.l); + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:Decal = new Decal(); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + } + + } +} diff --git a/src/alternativa/engine3d/objects/Joint.as b/src/alternativa/engine3d/objects/Joint.as new file mode 100644 index 0000000..a7cd7ef --- /dev/null +++ b/src/alternativa/engine3d/objects/Joint.as @@ -0,0 +1,126 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + + import flash.geom.Matrix3D; + + use namespace alternativa3d; + + /** + * A joint uses with Skin as handler for set of vertices. + * @see alternativa.engine3d.objects.Skin + */ + public class Joint extends Object3D { + + /** + * @private + * A joint transform matrix. + */ + alternativa3d var jointTransform:Transform3D = new Transform3D(); + + /** + * @private + */ + alternativa3d var bindPoseTransform:Transform3D = new Transform3D(); + + /** + * @private + */ + alternativa3d function setBindPoseMatrix(matrix:Vector.):void { + bindPoseTransform.initFromVector(matrix); + } + + public function get bindingMatrix():Matrix3D { + return new Matrix3D(Vector.([ + bindPoseTransform.a, bindPoseTransform.e, bindPoseTransform.i, 0, + bindPoseTransform.b, bindPoseTransform.f, bindPoseTransform.j, 0, + bindPoseTransform.c, bindPoseTransform.g, bindPoseTransform.k, 0, + bindPoseTransform.d, bindPoseTransform.h, bindPoseTransform.l, 1 + ])); + } + + public function set bindingMatrix(value:Matrix3D):void { + var data:Vector. = value.rawData; + bindPoseTransform.a = data[0]; + bindPoseTransform.b = data[4]; + bindPoseTransform.c = data[8]; + bindPoseTransform.d = data[12]; + bindPoseTransform.e = data[1]; + bindPoseTransform.f = data[5]; + bindPoseTransform.g = data[9]; + bindPoseTransform.h = data[13]; + bindPoseTransform.i = data[2]; + bindPoseTransform.j = data[6]; + bindPoseTransform.k = data[10]; + bindPoseTransform.l = data[14]; + } + + /** + * @private + */ + alternativa3d function calculateBindingMatrices():void { + for (var child:Object3D = childrenList; child != null; child = child.next) { + var joint:Joint = child as Joint; + if (joint != null) { + if (joint.transformChanged) { + joint.composeTransforms(); + } + joint.bindPoseTransform.combine(bindPoseTransform, joint.inverseTransform); + joint.calculateBindingMatrices(); + } + } + } + + + /** + * @private + */ + alternativa3d function calculateTransform():void { + if (bindPoseTransform != null) { + jointTransform.combine(localToGlobalTransform, bindPoseTransform); + } + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:Joint = new Joint(); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + var sourceJoint:Joint = source as Joint; + bindPoseTransform.a = sourceJoint.bindPoseTransform.a; + bindPoseTransform.b = sourceJoint.bindPoseTransform.b; + bindPoseTransform.c = sourceJoint.bindPoseTransform.c; + bindPoseTransform.d = sourceJoint.bindPoseTransform.d; + bindPoseTransform.e = sourceJoint.bindPoseTransform.e; + bindPoseTransform.f = sourceJoint.bindPoseTransform.f; + bindPoseTransform.g = sourceJoint.bindPoseTransform.g; + bindPoseTransform.h = sourceJoint.bindPoseTransform.h; + bindPoseTransform.i = sourceJoint.bindPoseTransform.i; + bindPoseTransform.j = sourceJoint.bindPoseTransform.j; + bindPoseTransform.k = sourceJoint.bindPoseTransform.k; + bindPoseTransform.l = sourceJoint.bindPoseTransform.l; + } + + } +} diff --git a/src/alternativa/engine3d/objects/LOD.as b/src/alternativa/engine3d/objects/LOD.as new file mode 100644 index 0000000..4d0fa08 --- /dev/null +++ b/src/alternativa/engine3d/objects/LOD.as @@ -0,0 +1,346 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.RayIntersectionData; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.events.Event3D; + + import flash.geom.Vector3D; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * Levels of detail is an Object3D which can have several representation of different detail level. + * The current level will be chosen for the rendering according to distance to the camera. + */ + public class LOD extends Object3D { + + /** + * @private + */ + alternativa3d var levelList:Object3D; + + /** + * Adds a children as a new level of detail. In case of given object is a children of other Object3D already, it will removed from the previous place. + * @param level Object3D which will added. + * @param distance If the LOD closer to the camera than distance value, this level will be preferred to the distant one. + * @return Object, given as lod parameter. + */ + public function addLevel(level:Object3D, distance:Number):Object3D { + // Checking for the errors. + if (level == null) throw new TypeError("Parameter level must be non-null."); + if (level == this) throw new ArgumentError("An object cannot be added as a child of itself."); + for (var container:Object3D = _parent; container != null; container = container._parent) { + if (container == level) throw new ArgumentError("An object cannot be added as a child to one of it's children (or children's children, etc.)."); + } + // Add. + if (level._parent != this) { + // Remove from previous parent. + if (level._parent != null) level._parent.removeChild(level); + // Add + addToLevelList(level, distance); + level._parent = this; + // Dispatch of event. + if (level.willTrigger(Event3D.ADDED)) level.dispatchEvent(new Event3D(Event3D.ADDED, true)); + } else { + if (removeFromList(level) == null) removeFromLevelList(level); + // Add. + addToLevelList(level, distance); + } + return level; + } + + /** + * Removes level of detail. + * + * @param level Object3d which was used as level of detail that will be removed. + * @return The Object3d instance that you pass in the level parameter. + * @see #addLevel() + */ + public function removeLevel(level:Object3D):Object3D { + // Checking for the errors. + if (level == null) throw new TypeError("Parameter level must be non-null."); + if (level._parent != this) throw new ArgumentError("The supplied Object3D must be a child of the caller."); + level = removeFromLevelList(level); + if (level == null) throw new ArgumentError("Cannot remove level."); + // Dispatch of event. + if (level.willTrigger(Event3D.REMOVED)) level.dispatchEvent(new Event3D(Event3D.REMOVED, true)); + level._parent = null; + return level; + } + + /** + * Returns distance was set up for the given level. If the LOD closer to the camera than distance, this level will be preferred to the distant one. + * @param level Object3d which was used as level of detail. + * @return Distance was set up for the given level. + */ + public function getLevelDistance(level:Object3D):Number { + // Checking for the errors + if (level == null) throw new TypeError("Parameter level must be non-null."); + if (level._parent != this) throw new ArgumentError("The supplied Object3D must be a child of the caller."); + for (var current:Object3D = levelList; current != null; current = current.next) { + if (level == current) return level.distance; + } + throw new ArgumentError("Cannot get level distance."); + } + + /** + * Sets distance to the given level. If the LOD closer to the camera than distance value, this level will be preffered to the distant one. + * @param level Object3d which was used as level of detail. + * @param distance Distance value. + */ + public function setLevelDistance(level:Object3D, distance:Number):void { + // Checking for the errors. + if (level == null) throw new TypeError("Parameter level must be non-null."); + if (level._parent != this) throw new ArgumentError("The supplied Object3D must be a child of the caller."); + level = removeFromLevelList(level); + if (level == null) throw new ArgumentError("Cannot set level distance."); + addToLevelList(level, distance); + } + + /** + * Returns Object3D which was used as level of detail at given distance. + * @param distance Distance. + * @return Object3D which was used as level of detail at given distance. + */ + public function getLevelByDistance(distance:Number):Object3D { + for (var current:Object3D = levelList; current != null; current = current.next) { + if (distance <= current.distance) return current; + } + return null; + } + + /** + * + * Returns Object3D which was used as level of detail with given name. + * @param name Name of the object. + * @return Object3D with given name. + */ + public function getLevelByName(name:String):Object3D { + // Checking for the errors. + if (name == null) throw new TypeError("Parameter name must be non-null."); + // Search for object + for (var current:Object3D = levelList; current != null; current = current.next) { + if (current.name == name) return current; + } + return null; + } + + /** + * Returns all Object3Ds which was used as levels of detail in this LOD, in Vector.<Object3D>. + * @return Vector.<Object3D> consists of Object3Ds which was used as levels of detail. + */ + public function getLevels():Vector. { + var res:Vector. = new Vector.(); + var num:int = 0; + for (var current:Object3D = levelList; current != null; current = current.next) { + res[num] = current; + num++; + } + return res; + } + + /** + * Number of levels of detail. + */ + public function get numLevels():int { + var num:int = 0; + for (var current:Object3D = levelList; current != null; current = current.next) num++; + return num; + } + + /** + * @private + */ + override alternativa3d function get useLights():Boolean { + return true; + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + var distance:Number = Math.sqrt(localToCameraTransform.d*localToCameraTransform.d + localToCameraTransform.h*localToCameraTransform.h + localToCameraTransform.l*localToCameraTransform.l); + for (var level:Object3D = levelList; level != null; level = level.next) { + if (distance <= level.distance) { + collectChildDraws(level, this, camera, lights, lightsLength); + break; + } + } + } + + /** + * @private + */ + alternativa3d function collectChildDraws(child:Object3D, parent:Object3D, camera:Camera3D, lights:Vector., lightsLength:int):void { + // Composing direct and reverse matrices + if (child.transformChanged) child.composeTransforms(); + // Calculation of transfer matrix from camera to local space. + child.cameraToLocalTransform.combine(child.inverseTransform, parent.cameraToLocalTransform); + // Calculation of transfer matrix from local space to camera. + child.localToCameraTransform.combine(parent.localToCameraTransform, child.transform); + // Pass + child.culling = parent.culling; + child.listening = parent.listening; + // If object needs on light sources. + if (lightsLength > 0 && child.useLights) { + // Calculation of transfer matrices from sources to object. + for (var i:int = 0; i < lightsLength; i++) { + var light:Light3D = lights[i]; + light.lightToObjectTransform.combine(child.cameraToLocalTransform, light.localToCameraTransform); + } + child.collectDraws(camera, lights, lightsLength); + } else { + child.collectDraws(camera, null, 0); + } + // Hierarchical call + for (var c:Object3D = child.childrenList; c != null; c = c.next) { + collectChildDraws(c, child, camera, lights, lightsLength); + } + } + + /** + * @private + */ + override alternativa3d function fillResources(resources:Dictionary, hierarchy:Boolean = false, resourceType:Class = null):void { + if (hierarchy) { + for (var current:Object3D = levelList; current != null; current = current.next) { + current.fillResources(resources, hierarchy, resourceType); + } + } + super.fillResources(resources, hierarchy, resourceType); + } + + /** + * @inheritDoc + */ + override public function intersectRay(origin:Vector3D, direction:Vector3D):RayIntersectionData { + var childrenData:RayIntersectionData = super.intersectRay(origin, direction); + var contentData:RayIntersectionData; + if (levelList != null && (boundBox == null || boundBox.intersectRay(origin, direction))) { + if (levelList.transformChanged) levelList.composeTransforms(); + var childOrigin:Vector3D = new Vector3D(); + var childDirection:Vector3D = new Vector3D(); + childOrigin.x = levelList.inverseTransform.a*origin.x + levelList.inverseTransform.b*origin.y + levelList.inverseTransform.c*origin.z + levelList.inverseTransform.d; + childOrigin.y = levelList.inverseTransform.e*origin.x + levelList.inverseTransform.f*origin.y + levelList.inverseTransform.g*origin.z + levelList.inverseTransform.h; + childOrigin.z = levelList.inverseTransform.i*origin.x + levelList.inverseTransform.j*origin.y + levelList.inverseTransform.k*origin.z + levelList.inverseTransform.l; + childDirection.x = levelList.inverseTransform.a*direction.x + levelList.inverseTransform.b*direction.y + levelList.inverseTransform.c*direction.z; + childDirection.y = levelList.inverseTransform.e*direction.x + levelList.inverseTransform.f*direction.y + levelList.inverseTransform.g*direction.z; + childDirection.z = levelList.inverseTransform.i*direction.x + levelList.inverseTransform.j*direction.y + levelList.inverseTransform.k*direction.z; + contentData = levelList.intersectRay(childOrigin, childDirection); + } + if (childrenData != null) { + if (contentData != null) { + return childrenData.time < contentData.time ? childrenData : contentData; + } else { + return childrenData; + } + } else { + return contentData; + } + } + + /** + * @private + */ + override alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + for (var current:Object3D = levelList; current != null; current = current.next) { + if (current.transformChanged) current.composeTransforms(); + if (transform != null) { + current.localToCameraTransform.combine(transform, current.transform); + } else { + current.localToCameraTransform.copy(current.transform); + } + current.updateBoundBox(boundBox, current.localToCameraTransform); + updateBoundBoxChildren(current, boundBox); + } + } + + private function updateBoundBoxChildren(parent:Object3D, boundBox:BoundBox):void { + for (var current:Object3D = parent.childrenList; current != null; current = current.next) { + if (current.transformChanged) current.composeTransforms(); + current.localToCameraTransform.combine(parent.localToCameraTransform, current.transform); + current.updateBoundBox(boundBox, current.localToCameraTransform); + updateBoundBoxChildren(current, boundBox); + } + } + + private function addToLevelList(level:Object3D, distance:Number):void { + level.distance = distance; + var prev:Object3D = null; + for (var current:Object3D = levelList; current != null; current = current.next) { + if (distance < current.distance) { + level.next = current; + break; + } + prev = current; + } + if (prev != null) { + prev.next = level; + } else { + levelList = level; + } + } + + private function removeFromLevelList(level:Object3D):Object3D { + var prev:Object3D; + for (var current:Object3D = levelList; current != null; current = current.next) { + if (current == level) { + if (prev != null) { + prev.next = current.next; + } else { + levelList = current.next; + } + current.next = null; + return level; + } + prev = current; + } + return null; + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:LOD = new LOD(); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + var src:LOD = source as LOD; + for (var current:Object3D = src.levelList, last:Object3D; current != null; current = current.next) { + var newLevel:Object3D = current.clone(); + if (levelList != null) { + last.next = newLevel; + } else { + levelList = newLevel; + } + last = newLevel; + newLevel._parent = this; + newLevel.distance = current.distance; + } + } + + } +} diff --git a/src/alternativa/engine3d/objects/Mesh.as b/src/alternativa/engine3d/objects/Mesh.as new file mode 100644 index 0000000..d0ed2a3 --- /dev/null +++ b/src/alternativa/engine3d/objects/Mesh.as @@ -0,0 +1,195 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.collisions.EllipsoidCollider; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.RayIntersectionData; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.resources.Geometry; + + import flash.geom.Vector3D; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * A polygonal object defined by set of vertices and surfaces built on this vertices. Surface is a set of triangles which have same material. + * To get access to vertices data you should use geometry property. + */ + public class Mesh extends Object3D { + + /** + * Through geometry property you can get access to vertices. + * @see alternativa.engine3d.resources.Geometry + */ + public var geometry:Geometry; + + /** + * @private + */ + alternativa3d var _surfaces:Vector. = new Vector.(); + /** + * @private + */ + alternativa3d var _surfacesLength:int = 0; + + /** + * @inheritDoc + */ + override public function intersectRay(origin:Vector3D, direction:Vector3D):RayIntersectionData { + var childrenData:RayIntersectionData = super.intersectRay(origin, direction); + var contentData:RayIntersectionData; + if (geometry != null && (boundBox == null || boundBox.intersectRay(origin, direction))) { + var minTime:Number = 1e22; + for each (var surface:Surface in _surfaces) { + var data:RayIntersectionData = geometry.intersectRay(origin, direction, surface.indexBegin, surface.numTriangles); + if (data != null && data.time < minTime) { + contentData = data; + contentData.object = this; + contentData.surface = surface; + minTime = data.time; + } + } + } + if (childrenData != null) { + if (contentData != null) { + return childrenData.time < contentData.time ? childrenData : contentData; + } else { + return childrenData; + } + } else { + return contentData; + } + } + + /** + * Adds Surface to Mesh object. + * @param material Material of the surface. + * @param indexBegin Position of the firs index of surface in the geometry. + * @param numTriangles Number of triangles. + */ + public function addSurface(material:Material, indexBegin:uint, numTriangles:uint):Surface { + var res:Surface = new Surface(); + res.object = this; + res.material = material; + res.indexBegin = indexBegin; + res.numTriangles = numTriangles; + _surfaces[_surfacesLength++] = res; + return res; + } + + /** + * Returns surface by index. + * + * @param index Index. + * @return Surface with given index. + */ + public function getSurface(index:int):Surface { + return _surfaces[index]; + } + + /** + * Number of surfaces. + */ + public function get numSurfaces():int { + return _surfacesLength; + } + + /** + * Assign given material to all surfaces. + * + * @param material Material. + * @see alternativa.engine3d.objects.Surface + * @see alternativa.engine3d.materials + */ + public function setMaterialToAllSurfaces(material:Material):void { + for (var i:int = 0; i < _surfaces.length; i++) { + _surfaces[i].material = material; + } + } + + /** + * @private + */ + override alternativa3d function get useLights():Boolean { + return true; + } + + /** + * @private + */ + override alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + if (geometry != null) geometry.updateBoundBox(boundBox, transform); + } + + /** + * @private + */ + alternativa3d override function fillResources(resources:Dictionary, hierarchy:Boolean = false, resourceType:Class = null):void { + if (geometry != null && (resourceType == null || geometry is resourceType)) resources[geometry] = true; + for (var i:int = 0; i < _surfacesLength; i++) { + var s:Surface = _surfaces[i]; + if (s.material != null) s.material.fillResources(resources, resourceType); + } + super.fillResources(resources, hierarchy, resourceType); + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + for (var i:int = 0; i < _surfacesLength; i++) { + var surface:Surface = _surfaces[i]; + if (surface.material != null) surface.material.collectDraws(camera, surface, geometry, lights, lightsLength); + // Mouse events + if (listening) camera.view.addSurfaceToMouseEvents(surface, geometry, transformProcedure); + } + } + + /** + * @private + */ + override alternativa3d function collectGeometry(collider:EllipsoidCollider, excludedObjects:Dictionary):void { + collider.geometries.push(geometry); + collider.transforms.push(localToGlobalTransform); + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:Mesh = new Mesh(); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + var mesh:Mesh = source as Mesh; + geometry = mesh.geometry; + _surfacesLength = 0; + _surfaces.length = 0; + for each (var s:Surface in mesh._surfaces) { + addSurface(s.material, s.indexBegin, s.numTriangles); + } + } + + } +} diff --git a/src/alternativa/engine3d/objects/MeshSet.as b/src/alternativa/engine3d/objects/MeshSet.as new file mode 100644 index 0000000..3197d6a --- /dev/null +++ b/src/alternativa/engine3d/objects/MeshSet.as @@ -0,0 +1,263 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Debug; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.VertexStream; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.resources.Geometry; + + import flash.display3D.Context3DVertexBufferFormat; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * @private + */ + public class MeshSet extends Mesh { + private var root:Object3D; + + private static const ATTRIBUTE:uint = 20; + + private var surfaceMeshes:Vector.> = new Vector.>(); + + public static const MESHES_PER_SURFACE:uint = 30; + private var surfaceTransformProcedures:Vector. = new Vector.(); + private var surfaceDeltaTransformProcedures:Vector. = new Vector.(); + + private static var _transformProcedures:Dictionary = new Dictionary(); + private static var _deltaTransformProcedures:Dictionary = new Dictionary(); + + public function MeshSet(root:Object3D) { + this.root = root; + calculateGeometry(); + } + + alternativa3d override function calculateVisibility(camera:Camera3D):void { + super.alternativa3d::calculateVisibility(camera); + if (root.transformChanged) root.composeTransforms(); + root.localToGlobalTransform.copy(root.transform); + calculateMeshesTransforms(root); + } + + alternativa3d override function setTransformConstants(drawUnit:DrawUnit, surface:Surface, vertexShader:Linker, camera:Camera3D):void { + drawUnit.setVertexBufferAt(vertexShader.getVariableIndex("joint"), geometry.getVertexBuffer(ATTRIBUTE), geometry._attributesOffsets[ATTRIBUTE], Context3DVertexBufferFormat.FLOAT_1); + var index:uint = _surfaces.indexOf(surface); + var meshes:Vector. = surfaceMeshes[index]; + for (var i:int = 0, count:int = meshes.length; i < count; i++) { + var mesh:Mesh = meshes[i]; + drawUnit.setVertexConstantsFromTransform(i*3, mesh.localToGlobalTransform); + } + } + + private function calculateMeshesTransforms(root:Object3D):void { + for (var child:Object3D = root.childrenList; child != null; child = child.next) { + if (child.transformChanged) child.composeTransforms(); + // Put skin transfer matrix to localToGlobalTransform + child.localToGlobalTransform.combine(root.localToGlobalTransform, child.transform); + calculateMeshesTransforms(child); + } + } + + override alternativa3d function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + if (geometry == null) return; + // Calculation of joints matrices. + for (var i:int = 0; i < _surfacesLength; i++) { + var surface:Surface = _surfaces[i]; + transformProcedure = surfaceTransformProcedures[i]; + deltaTransformProcedure = surfaceDeltaTransformProcedures[i]; + if (surface.material != null) surface.material.collectDraws(camera, surface, geometry, lights, lightsLength); + // Mouse events + if (listening) camera.view.addSurfaceToMouseEvents(surface, geometry, transformProcedure); + } + // Debug + if (camera.debug) { + var debug:int = camera.checkInDebug(this); + if ((debug & Debug.BOUNDS) && boundBox != null) Debug.drawBoundBox(camera, boundBox, localToCameraTransform); + } + } + + private function calculateGeometry():void { + geometry = new Geometry(0); + addSurface(null, 0, 0); + var numAttributes:int = 32; + var attributesDict:Vector. = new Vector.(numAttributes, true); + var attributesLengths:Vector. = new Vector.(numAttributes, true); + var numMeshes:Number = collectAttributes(root, attributesDict, attributesLengths); + + var attributes:Array = new Array(); + var i:int; + + for (i = 0; i < numAttributes; i++) { + if (attributesDict[i] > 0) { + attributesLengths[i] = attributesLengths[i]/attributesDict[i]; + } + } + for (i = 0; i < numAttributes; i++) { + if (Number(attributesDict[i])/numMeshes == 1) { + for (var j:int = 0; j < attributesLengths[i]; j++) { + attributes.push(i); + } + + } + } + attributes.push(ATTRIBUTE); + geometry.addVertexStream(attributes); + if (root is Mesh) appendMesh(root as Mesh); + collectMeshes(root); + var surfaceIndex:uint = _surfaces.length - 1; + var meshes:Vector. = surfaceMeshes[surfaceIndex]; + surfaceTransformProcedures[surfaceIndex] = calculateTransformProcedure(meshes.length); + surfaceDeltaTransformProcedures[surfaceIndex] = calculateDeltaTransformProcedure(meshes.length); + } + + private function collectAttributes(root:Object3D, attributesDict:Vector., attributesLengths:Vector.):int { + var geom:Geometry; + var numMeshes:int = 0; + if (root is Mesh) { + geom = Mesh(root).geometry; + + for each (var stream:VertexStream in geom._vertexStreams) { + var prev:int = -1; + var attributes:Array = stream.attributes; + for each (var attr:int in attributes) { + attributesLengths[attr]++; + if (attr == prev) continue; + attributesDict[attr]++; + prev = attr; + } + } + numMeshes++; + } + + for (var child:Object3D = root.childrenList; child != null; child = child.next) { + numMeshes += collectAttributes(child, attributesDict, attributesLengths); + } + return numMeshes; + } + + override public function addSurface(material:Material, indexBegin:uint, numTriangles:uint):Surface { + surfaceMeshes.push(new Vector.()); + return super.addSurface(material, indexBegin, numTriangles); + } + + private function collectMeshes(root:Object3D):void { + for (var child:Object3D = root.childrenList; child != null; child = child.next) { + if (child is Mesh) { + appendMesh(child as Mesh); + } + collectMeshes(child); + } + } + + private function appendGeometry(geom:Geometry, index:int):void { + var stream:VertexStream; + var i:int, j:int; + var length:uint = geom._vertexStreams.length; + var numVertices:int = geom._numVertices; + for (i = 0; i < length; i++) { + stream = geom._vertexStreams[i]; + var attributes:Array = geometry._vertexStreams[i].attributes; + var attribtuesLength:int = attributes.length; + var destStream:VertexStream = geometry._vertexStreams[i]; + var newOffset:int = destStream.data.length; + destStream.data.position = newOffset; + + stream.data.position = 0; + var stride:int = stream.attributes.length*4; + var destStride:int = destStream.attributes.length*4; + for (j = 0; j < numVertices; j++) { + var prev:int = -1; + for (var k:int = 0; k < attribtuesLength; k++) { + var attr:int = attributes[k]; + if (attr == ATTRIBUTE) { + destStream.data.writeFloat(index*3); + continue; + } + if (attr != prev) { + stream.data.position = geom._attributesOffsets[attr]*4 + stride*j; + destStream.data.position = newOffset + geometry._attributesOffsets[attr]*4 + destStride*j; + } + destStream.data.writeFloat(stream.data.readFloat()); + prev = attr; + } + } + + } + geometry._numVertices += geom._numVertices; + + } + + private function compareAttribtues(destStream:VertexStream, sourceStream:VertexStream):Boolean { + if ((destStream.attributes.length - 1) != sourceStream.attributes.length) return false; + var len:int = sourceStream.attributes.length; + for (var i:int = 0; i < len; i++) { + if (destStream.attributes[i] != sourceStream.attributes[i]) return false; + } + return true; + } + + private function appendMesh(mesh:Mesh):void { + var surfaceIndex:uint = _surfaces.length - 1; + var destSurface:Surface = _surfaces[surfaceIndex]; + var meshes:Vector. = surfaceMeshes[surfaceIndex]; + if (meshes.length >= MESHES_PER_SURFACE) { + surfaceTransformProcedures[surfaceIndex] = calculateTransformProcedure(meshes.length); + surfaceDeltaTransformProcedures[surfaceIndex] = calculateDeltaTransformProcedure(meshes.length); + addSurface(null, geometry._indices.length, 0); + surfaceIndex++; + destSurface = _surfaces[surfaceIndex]; + meshes = surfaceMeshes[surfaceIndex]; + } + meshes.push(mesh); + var geom:Geometry = mesh.geometry; + var vertexOffset:uint; + var i:int, j:int; + vertexOffset = geometry._numVertices; + appendGeometry(geom, meshes.length - 1); + trace(surfaceIndex); + // Copy indexes + for (i = 0; i < mesh._surfacesLength; i++) { + var surface:Surface = mesh._surfaces[i]; + var indexEnd:uint = surface.numTriangles*3 + surface.indexBegin; + destSurface.numTriangles += surface.numTriangles; + for (j = surface.indexBegin; j < indexEnd; j++) { + geometry._indices.push(geom._indices[j] + vertexOffset); + } + } + } + + private function calculateTransformProcedure(numMeshes:int):Procedure { + var res:Procedure = _transformProcedures[numMeshes]; + if (res != null) return res; + res = _transformProcedures[numMeshes] = new Procedure(null, "MeshSetTransformProcedure"); + res.compileFromArray(["#a0=joint", "m34 o0.xyz, i0, c[a0.x]", "mov o0.w, i0.w"]); + res.assignConstantsArray(numMeshes*3); + return res; + } + + private function calculateDeltaTransformProcedure(numMeshes:int):Procedure { + var res:Procedure = _deltaTransformProcedures[numMeshes]; + if (res != null) return res; + res = _deltaTransformProcedures[numMeshes] = new Procedure(null, "MeshSetDeltaTransformProcedure"); + res.compileFromArray(["#a0=joint", "m33 o0.xyz, i0, c[a0.x]", "mov o0.w, i0.w"]); + return res; + } + } +} diff --git a/src/alternativa/engine3d/objects/Skin.as b/src/alternativa/engine3d/objects/Skin.as new file mode 100644 index 0000000..b5b31b0 --- /dev/null +++ b/src/alternativa/engine3d/objects/Skin.as @@ -0,0 +1,733 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.core.VertexStream; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.resources.Geometry; + + import flash.utils.ByteArray; + import flash.utils.Dictionary; + import flash.utils.Endian; + + use namespace alternativa3d; + + /** + * Skin is a Mesh which can have a skeleton besides surfaces. The skeleton is a hierarchy of bones, represented by Joint class. + * Each bone can be linked with set of vertices, thus, position of the bone will affect to position of linked vertices. Set of positions of all skeleton bones + * defines a pose which also defines pose of skin's surfaces. Character animation implements through making sequence of such poses. + * If number of bones which affect one surface more than fixed number, skin would not be drawn. + * "Fixed number" defines by the material of given surface, look documentation of the material class for it. + * To avoid this problem use Skin.divide() method. + * + * If you creates a skeleton within a code, make sure that each bone added to: + * 1) skin hierarchy with addChild() method, + * 2) renderedJoints property which represented by Vector.<Joint>. + * + * Link of a vertex and a bone stores in vertex data of VertexAttributes.JOINTS type. + * Vertex buffer point to a joint with following form: index of the joint within renderedJoints multiplied with 3. + * It is done so in order to avoid this multiplication within vertex shader for each frame. + * + * @see alternativa.engine3d.core.VertexAttributes#JOINTS + * @see alternativa.engine3d.objects.Joint + * @see #divide() + */ + public class Skin extends Mesh { + + /** + * @private + */ + alternativa3d var _renderedJoints:Vector.; + + /** + * @private + */ + alternativa3d var surfaceJoints:Vector.>; + + /** + * @private + */ + alternativa3d var surfaceTransformProcedures:Vector.; + + /** + * @private + */ + alternativa3d var surfaceDeltaTransformProcedures:Vector.; + + /** + * @private + */ + alternativa3d var maxInfluences:int = 0; + + // key = maxInfluences | numJoints << 16 + private static var _transformProcedures:Dictionary = new Dictionary(); + // Cashing of procedures on number of influence + private static var _deltaTransformProcedures:Vector. = new Vector.(9); + + /** + * Creates a new Skin instance. + * @param maxInfluences Max number of bones that can affect one vertex. + */ + public function Skin(maxInfluences:int) { + this.maxInfluences = maxInfluences; + + surfaceJoints = new Vector.>(); + surfaceTransformProcedures = new Vector.(); + surfaceDeltaTransformProcedures = new Vector.(); + } + + public function calculateBindingMatrices():void { + for (var child:Object3D = childrenList; child != null; child = child.next) { + var joint:Joint = child as Joint; + if (joint != null) { + if (joint.transformChanged) { + joint.composeTransforms(); + } + joint.bindPoseTransform.copy(joint.inverseTransform); + joint.calculateBindingMatrices(); + } + } + } + + /** + * @inheritDoc + */ + override public function addSurface(material:Material, indexBegin:uint, numTriangles:uint):Surface { + surfaceJoints[_surfacesLength] = _renderedJoints; + surfaceTransformProcedures[_surfacesLength] = transformProcedure; + surfaceDeltaTransformProcedures[_surfacesLength] = deltaTransformProcedure; + return super.addSurface(material, indexBegin, numTriangles); + } + + private function divideSurface(limit:uint, iterations:uint, surface:Surface, jointsOffsets:Vector., jointBufferVertexSize:uint, inVertices:ByteArray, outVertices:ByteArray, outIndices:Vector., outSurfaces:Vector., outJointsMaps:Vector.):uint { + var indexBegin:uint = surface.indexBegin; + var indexCount:uint = surface.numTriangles*3; + var i:int, j:int, count:int, jointsLength:int, index:uint; + var indices:Vector. = geometry._indices; + var groups:Dictionary = new Dictionary(); + var group:Dictionary; + + var key:*, key2:*; + var jointIndex:uint; + var weight:Number; + for (i = indexBegin,count = indexBegin + indexCount; i < count; i += 3) { + group = groups[i] = new Dictionary(); + var jointsGroupLength:uint = 0; + for (var n:int = 0; n < 3; n++) { + index = indices[int(i + n)]; + for (j = 0,jointsLength = jointsOffsets.length; j < jointsLength; j++) { + inVertices.position = jointBufferVertexSize*index + jointsOffsets[j]; + jointIndex = uint(inVertices.readFloat()); + weight = inVertices.readFloat(); + if (weight > 0) { + group[jointIndex] = true; + } + } + } + for (key in group) { + jointsGroupLength++; + } + if (jointsGroupLength > limit) { + throw new Error("Unable to divide Skin."); + } + } + var localNumJoints:uint; + + var facesGroups:Dictionary = optimizeGroups(groups, limit, iterations); + var newIndex:uint = 0; + var newIndexBegin:uint; + for (key in facesGroups) { + var faces:Dictionary = facesGroups[key]; + localNumJoints = 0; + group = groups[key]; + for (key2 in group) { + if (group[key2] is Boolean) { + group[key2] = 3*localNumJoints++; + } + } + var locatedIndices:Dictionary = new Dictionary(); + for (key2 in faces) { + for (i = 0; i < 3; i++) { + index = indices[int(key2 + i)]; + if (locatedIndices[index] != null) { + outIndices.push(locatedIndices[index]); + continue; + } + locatedIndices[index] = newIndex; + outIndices.push(newIndex++); + outVertices.writeBytes(inVertices, index*jointBufferVertexSize, jointBufferVertexSize); + outVertices.position -= jointBufferVertexSize; + var origin:uint = outVertices.position; + var sumWeight:Number = 0; + // reindexation of bones + for (j = 0; j < jointsLength; j++) { + outVertices.position = origin + jointsOffsets[j]; + jointIndex = uint(outVertices.readFloat()); + weight = outVertices.readFloat(); + outVertices.position -= 8; + if (weight > 0) { + outVertices.writeFloat(group[jointIndex]); + outVertices.writeFloat(weight); + sumWeight += weight; + } + } + // normalization of weights + if (sumWeight != 1) { + for (j = 0; j < jointsLength; j++) { + outVertices.position = origin + jointsOffsets[j] + 4; + weight = outVertices.readFloat(); + if (weight > 0) { + outVertices.position -= 4; + outVertices.writeFloat(weight/sumWeight); + } + } + } + outVertices.position = origin + jointBufferVertexSize; + } + } + var resSurface:Surface = new Surface(); + resSurface.object = this; + resSurface.material = surface.material; + resSurface.indexBegin = newIndexBegin; + resSurface.numTriangles = (outIndices.length - newIndexBegin)/3; + outSurfaces.push(resSurface); + outJointsMaps.push(group); + newIndexBegin = outIndices.length; + } + return newIndex; + } + + /** + * Union of groups. + * @groups Set of groups for merging. + * @limit Max number of joints per group. + * @iterations Number of algorythm iteration (more iterations - better result). + */ + private function optimizeGroups(groups:Dictionary, limit:uint, iterations:uint = 1):Dictionary { + var key:*; + var inKey:*; + var facesGroups:Dictionary = new Dictionary(); + for (var i:int = 1; i < iterations + 1; i++) { + var minLike:Number = 1 - i/iterations; + for (key in groups) { + var group1:Dictionary = groups[key]; + for (inKey in groups) { + if (key == inKey) continue; + var group2:Dictionary = groups[inKey]; + var like:Number = calculateLikeFactor(group1, group2, limit); + if (like >= minLike) { + delete groups[inKey]; + for (var copyKey:* in group2) { + group1[copyKey] = true; + } + var indices:Dictionary = facesGroups[key]; + if (indices == null) { + indices = facesGroups[key] = new Dictionary(); + indices[key] = true; + } + + var indices2:Dictionary = facesGroups[inKey]; + if (indices2 != null) { + delete facesGroups[inKey]; + for (copyKey in indices2) { + indices[copyKey] = true; + } + } else { + indices[inKey] = true; + } + } + } + } + } + return facesGroups; + } + + // Calculates "level of similarity" of two groups + private function calculateLikeFactor(group1:Dictionary, group2:Dictionary, limit:uint):Number { + var key:*; + var unionCount:uint; + var intersectCount:uint; + var group1Count:uint; + var group2Count:uint; + for (key in group1) { + unionCount++; + if (group2[key] != null) { + intersectCount++; + } + group1Count++; + } + for (key in group2) { + if (group1[key] == null) { + unionCount++ + } + group2Count++; + } + if (unionCount > limit) return -1; + return intersectCount/unionCount; + } + + /** + * Subdivides skin surfaces. It can be useful in case of impossibility to render a skin due to too big number of bones affected to one surface. (In this case appropriate exception will generated). + * @param limit No more than limit of bones can have its own surface. I.e. if skin instance has 6 joints and limit = 3, + * it will divided into 2 surface and if limit = 6 - into 6 surfaces. + * @param iterations Number of iterations. Increase accuracy and execution time. + */ + public function divide(limit:uint, iterations:uint = 1):void { + if (_renderedJoints == null || maxInfluences <= 0) return; + // Checking: are all joints at one vertex-buffer? + var jointsBuffer:int = geometry.findVertexStreamByAttribute(VertexAttributes.JOINTS[0]); + var jointsOffsets:Vector. = new Vector.(); + var jointOffset:int = 0; + if (jointsBuffer >= 0) { + jointOffset = geometry.getAttributeOffset(VertexAttributes.JOINTS[0])*4; + jointsOffsets.push(jointOffset); + jointsOffsets.push(jointOffset + 8); + } else { + throw new Error("Cannot divide skin, joints[0] must be binded"); + } + var jbTest:int = geometry.findVertexStreamByAttribute(VertexAttributes.JOINTS[1]); + if (jbTest >= 0) { + jointOffset = geometry.getAttributeOffset(VertexAttributes.JOINTS[1])*4; + jointsOffsets.push(jointOffset); + jointsOffsets.push(jointOffset + 8); + if (jointsBuffer != jbTest) { + throw new Error("Cannot divide skin, all joinst must be in the same buffer"); + } + } + + jbTest = geometry.findVertexStreamByAttribute(VertexAttributes.JOINTS[2]); + + if (jbTest >= 0) { + jointOffset = geometry.getAttributeOffset(VertexAttributes.JOINTS[2])*4; + jointsOffsets.push(jointOffset); + jointsOffsets.push(jointOffset + 8); + if (jointsBuffer != jbTest) { + throw new Error("Cannot divide skin, all joinst must be in the same buffer"); + } + } + + jbTest = geometry.findVertexStreamByAttribute(VertexAttributes.JOINTS[3]); + + if (jbTest >= 0) { + jointOffset = geometry.getAttributeOffset(VertexAttributes.JOINTS[3])*4; + jointsOffsets.push(jointOffset); + jointsOffsets.push(jointOffset + 8); + if (jointsBuffer != jbTest) { + throw new Error("Cannot divide skin, all joinst must be in the same buffer"); + } + } + var outSurfaces:Vector. = new Vector.(); + var totalVertices:ByteArray = new ByteArray(); + totalVertices.endian = Endian.LITTLE_ENDIAN; + var totalIndices:Vector. = new Vector.(); + var totalIndicesLength:uint = 0; + var lastMaxIndex:uint = 0; + var key:*; + var lastSurfaceIndex:uint = 0; + var lastIndicesCount:uint = 0; + surfaceJoints.length = 0; + var jointsBufferNumMappings:int = geometry._vertexStreams[jointsBuffer].attributes.length; + var jointsBufferData:ByteArray = geometry._vertexStreams[jointsBuffer].data; + for (var i:int = 0; i < _surfacesLength; i++) { + var outIndices:Vector. = new Vector.(); + var outVertices:ByteArray = new ByteArray(); + var outJointsMaps:Vector. = new Vector.(); + outVertices.endian = Endian.LITTLE_ENDIAN; + var maxIndex:uint = divideSurface(limit, iterations, _surfaces[i], jointsOffsets, + jointsBufferNumMappings*4, jointsBufferData, outVertices, outIndices, outSurfaces, outJointsMaps); + for (var j:int = 0, count:int = outIndices.length; j < count; j++) { + totalIndices[totalIndicesLength++] = lastMaxIndex + outIndices[j]; + } + + for (j = 0,count = outJointsMaps.length; j < count; j++) { + var maxJoints:uint = 0; + var vec:Vector. = surfaceJoints[j + lastSurfaceIndex] = new Vector.(); + var joints:Dictionary = outJointsMaps[j]; + for (key in joints) { + var index:uint = uint(joints[key]/3); + if (vec.length < index) vec.length = index + 1; + vec[index] = _renderedJoints[uint(key/3)]; + maxJoints++; + } + } + for (j = lastSurfaceIndex; j < outSurfaces.length; j++) { + outSurfaces[j].indexBegin += lastIndicesCount; + + } + lastSurfaceIndex += outJointsMaps.length; + lastIndicesCount += outIndices.length; + totalVertices.writeBytes(outVertices, 0, outVertices.length); + lastMaxIndex += maxIndex; + } + _surfaces = outSurfaces; + _surfacesLength = outSurfaces.length; + surfaceTransformProcedures.length = _surfacesLength; + surfaceDeltaTransformProcedures.length = _surfacesLength; + calculateSurfacesProcedures(); + var newGeometry:Geometry = new Geometry(); + newGeometry._indices = totalIndices; + + for (i = 0; i < geometry._vertexStreams.length; i++) { + var attributes:Array = geometry._vertexStreams[i].attributes; + newGeometry.addVertexStream(attributes); + if (i == jointsBuffer) { + newGeometry._vertexStreams[i].data = totalVertices; + } else { + var data:ByteArray = new ByteArray(); + data.endian = Endian.LITTLE_ENDIAN; + data.writeBytes(geometry._vertexStreams[i].data); + newGeometry._vertexStreams[i].data = data; + } + } + newGeometry._numVertices = totalVertices.length/(newGeometry._vertexStreams[0].attributes.length << 2); + geometry = newGeometry; + } + + /** + * @private + */ + alternativa3d function calculateJointsTransforms(root:Object3D):void { + for (var child:Object3D = root.childrenList; child != null; child = child.next) { + if (child.transformChanged) child.composeTransforms(); + // Write transformToSkin matrix to localToGlobalTransform property + child.localToGlobalTransform.combine(root.localToGlobalTransform, child.transform); + if (child is Joint) { + Joint(child).calculateTransform(); + } + calculateJointsTransforms(child); + } + } + + /** + * @private + */ + override alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + for (var child:Object3D = childrenList; child != null; child = child.next) { + if (child.transformChanged) child.composeTransforms(); + // Write transformToSkin matrix to localToGlobalTransform property + child.localToGlobalTransform.copy(child.transform); + if (child is Joint) { + Joint(child).calculateTransform(); + } + calculateJointsTransforms(child); + } + var vertexSurface:Dictionary = new Dictionary(); + var indices:Vector. = geometry._indices; + // Fill the map vertex-surface + for (var i:int = 0; i < _surfacesLength; i++) { + var surface:Surface = _surfaces[i]; + for (var j:int = surface.indexBegin, count:int = surface.indexBegin + surface.numTriangles*3; j < count; j++) { + vertexSurface[indices[j]] = i; + } + } + var joints:Vector.; + var positions:VertexStream = geometry._attributesStreams[VertexAttributes.POSITION]; + var positionOffset:int = geometry._attributesOffsets[VertexAttributes.POSITION]*4; + var jointsStreams:Vector. = new Vector.(); + var jointsOffsets:Vector. = new Vector.(); + for (i = 0; i < 4; i++) { + if (geometry.hasAttribute(VertexAttributes.JOINTS[i])) { + jointsStreams.push(geometry._attributesStreams[VertexAttributes.JOINTS[i]]); + jointsOffsets.push(geometry._attributesOffsets[VertexAttributes.JOINTS[i]]*4); + } + } + var jointsStreamsLength:uint = jointsStreams.length; + for (i = 0; i < geometry._numVertices; i++) { + joints = surfaceJoints[vertexSurface[i]]; + var buffer:ByteArray = positions.data; + buffer.position = positionOffset + i*positions.attributes.length*4; + + var x:Number = buffer.readFloat(); + var y:Number = buffer.readFloat(); + var z:Number = buffer.readFloat(); + var ox:Number = 0; + var oy:Number = 0; + var oz:Number = 0; + var tx:Number, ty:Number, tz:Number; + for (j = 0; j < jointsStreamsLength; j++) { + buffer = jointsStreams[j].data; + buffer.position = jointsOffsets[j] + i*jointsStreams[j].attributes.length*4; + var jointIndex1:int = buffer.readFloat(); + var jointWeight1:Number = buffer.readFloat(); + var jointIndex2:int = buffer.readFloat(); + var jointWeight2:Number = buffer.readFloat(); + var joint:Joint; + var trm:Transform3D; + if (jointWeight1 > 0) { + joint = joints[int(jointIndex1/3)]; + trm = joint.jointTransform; + tx = x*trm.a + y*trm.b + z*trm.c + trm.d; + ty = x*trm.e + y*trm.f + z*trm.g + trm.h; + tz = x*trm.i + y*trm.j + z*trm.k + trm.l; + ox += tx*jointWeight1; + oy += ty*jointWeight1; + oz += tz*jointWeight1; + } + if (jointWeight2 > 0) { + joint = joints[int(jointIndex2/3)]; + trm = joint.jointTransform; + tx = x*trm.a + y*trm.b + z*trm.c + trm.d; + ty = x*trm.e + y*trm.f + z*trm.g + trm.h; + tz = x*trm.i + y*trm.j + z*trm.k + trm.l; + ox += tx*jointWeight2; + oy += ty*jointWeight2; + oz += tz*jointWeight2; + } + } + + if (transform != null) { + tx = ox*transform.a + oy*transform.b + oz*transform.c + transform.d; + ty = ox*transform.e + oy*transform.f + oz*transform.g + transform.h; + tz = ox*transform.i + oy*transform.j + oz*transform.k + transform.l; + ox = tx; oy = ty; oz = tz; + } + + if (ox < boundBox.minX) { + boundBox.minX = ox; + } + + if (oy < boundBox.minY) { + boundBox.minY = oy; + } + + if (oz < boundBox.minZ) { + boundBox.minZ = oz; + } + + if (ox > boundBox.maxX) { + boundBox.maxX = ox; + } + + if (oy > boundBox.maxY) { + boundBox.maxY = oy; + } + + if (oz > boundBox.maxZ) { + boundBox.maxZ = oz; + } + } + } + + /** + * @private + */ + public function get renderedJoints():Vector. { + return _renderedJoints; + } + + /** + * @private + */ + public function set renderedJoints(value:Vector.):void { + //If skin is not divided, change number of bonesfor each surface + for (var i:int = 0; i < _surfacesLength; i++) { + if (surfaceJoints[i] == _renderedJoints) { + surfaceJoints[i] = value; + } + } + _renderedJoints = value; + + calculateSurfacesProcedures(); + } + + /** + * @private + * Recalculate procedures of surface transformation with respect to number of bones and their influences. + */ + alternativa3d function calculateSurfacesProcedures():void { + var numJoints:int = _renderedJoints != null ? _renderedJoints.length : 0; + transformProcedure = calculateTransformProcedure(maxInfluences, numJoints); + deltaTransformProcedure = calculateDeltaTransformProcedure(maxInfluences); + for (var i:int = 0; i < _surfacesLength; i++) { + numJoints = surfaceJoints[i] != null ? surfaceJoints[i].length : 0; + surfaceTransformProcedures[i] = calculateTransformProcedure(maxInfluences, numJoints); + surfaceDeltaTransformProcedures[i] = calculateDeltaTransformProcedure(maxInfluences); + } + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + if (geometry == null) return; + // Calculate joints matrices + for (var child:Object3D = childrenList; child != null; child = child.next) { + if (child.transformChanged) child.composeTransforms(); + // Write transformToSkin matrix to localToGlobalTransform property + child.localToGlobalTransform.copy(child.transform); + if (child is Joint) { + Joint(child).calculateTransform(); + } + calculateJointsTransforms(child); + } + + for (var i:int = 0; i < _surfacesLength; i++) { + var surface:Surface = _surfaces[i]; + transformProcedure = surfaceTransformProcedures[i]; + deltaTransformProcedure = surfaceDeltaTransformProcedures[i]; + if (surface.material != null) surface.material.collectDraws(camera, surface, geometry, lights, lightsLength); + + /*var destination:DrawUnit = surface.getDrawUnit(camera, geometry, lights, lightsLength); + if (destination == null) continue; + camera.renderer.addDrawUnit(destination); + setTransformConstants(destination, surface, destination.program.vertexShader, camera);*/ + // Mouse events + if (listening) camera.view.addSurfaceToMouseEvents(surface, geometry, transformProcedure); + } + } + + /** + * @private + */ + override alternativa3d function setTransformConstants(drawUnit:DrawUnit, surface:Surface, vertexShader:Linker, camera:Camera3D):void { + var i:int, count:int; + for (i = 0; i < maxInfluences; i += 2) { + var attribute:int = VertexAttributes.JOINTS[i >> 1]; + drawUnit.setVertexBufferAt(vertexShader.getVariableIndex("joint" + i.toString()), geometry.getVertexBuffer(attribute), geometry._attributesOffsets[attribute], VertexAttributes.FORMATS[attribute]); + } + var surfaceIndex:int = _surfaces.indexOf(surface); + var joints:Vector. = surfaceJoints[surfaceIndex]; + for (i = 0,count = joints.length; i < count; i++) { + var joint:Joint = joints[i]; + drawUnit.setVertexConstantsFromTransform(i*3, joint.jointTransform); + } + } + + private function calculateTransformProcedure(maxInfluences:int, numJoints:int):Procedure { + var res:Procedure = _transformProcedures[maxInfluences | (numJoints << 16)]; + if (res != null) return res; + res = _transformProcedures[maxInfluences | (numJoints << 16)] = new Procedure(null, "SkinTransformProcedure"); + var array:Array = []; + var j:int = 0; + for (var i:int = 0; i < maxInfluences; i ++) { + var joint:int = int(i/2); + if (i%2 == 0) { + if (i == 0) { + array[j++] = "m34 t0.xyz, i0, c[a" + joint + ".x]"; + array[j++] = "mul o0, t0.xyz, a" + joint + ".y"; + } else { + array[j++] = "m34 t0.xyz, i0, c[a" + joint + ".x]"; + array[j++] = "mul t0.xyz, t0.xyz, a" + joint + ".y"; + array[j++] = "add o0, o0, t0.xyz"; + } + } else { + array[j++] = "m34 t0.xyz, i0, c[a" + joint + ".z]"; + array[j++] = "mul t0.xyz, t0.xyz, a" + joint + ".w"; + array[j++] = "add o0, o0, t0.xyz"; + } + } + array[j++] = "mov o0.w, i0.w"; + res.compileFromArray(array); + res.assignConstantsArray(numJoints*3); + for (i = 0; i < maxInfluences; i += 2) { + res.assignVariableName(VariableType.ATTRIBUTE, int(i/2), "joint" + i); + } + return res; + } + + private function calculateDeltaTransformProcedure(maxInfluences:int):Procedure { + var res:Procedure = _deltaTransformProcedures[maxInfluences]; + if (res != null) return res; + res = new Procedure(null, "SkinDeltaTransformProcedure"); + _deltaTransformProcedures[maxInfluences] = res; + var array:Array = []; + var j:int = 0; + for (var i:int = 0; i < maxInfluences; i ++) { + var joint:int = int(i/2); + if (i%2 == 0) { + if (i == 0) { + array[j++] = "m33 t0.xyz, i0, c[a" + joint + ".x]"; + array[j++] = "mul o0, t0.xyz, a" + joint + ".y"; + } else { + array[j++] = "m33 t0.xyz, i0, c[a" + joint + ".x]"; + array[j++] = "mul t0.xyz, t0.xyz, a" + joint + ".y"; + array[j++] = "add o0, o0, t0.xyz"; + } + } else { + array[j++] = "m33 t0.xyz, i0, c[a" + joint + ".z]"; + array[j++] = "mul t0.xyz, t0.xyz, a" + joint + ".w"; + array[j++] = "add o0, o0, t0.xyz"; + } + } + array[j++] = "mov o0.w, i0.w"; + array[j++] = "nrm o0.xyz, o0.xyz"; + res.compileFromArray(array); + for (i = 0; i < maxInfluences; i += 2) { + res.assignVariableName(VariableType.ATTRIBUTE, int(i/2), "joint" + i); + } + return res; + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:Skin = new Skin(maxInfluences); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + var skin:Skin = Skin(source); + this.maxInfluences = skin.maxInfluences; + if (skin._renderedJoints != null) { + // Clone renderedJoints + this._renderedJoints = cloneJointsVector(skin._renderedJoints, skin); + } + this.transformProcedure = skin.transformProcedure; + this.deltaTransformProcedure = skin.deltaTransformProcedure; + for (var i:int = 0; i < _surfacesLength; i++) { + surfaceJoints[i] = cloneJointsVector(skin.surfaceJoints[i], skin); + surfaceTransformProcedures[i] = skin.surfaceTransformProcedures[i]; + surfaceDeltaTransformProcedures[i] = skin.surfaceDeltaTransformProcedures[i]; + } + } + + private function cloneJointsVector(joints:Vector., skin:Skin):Vector. { + var count:int = joints.length; + var result:Vector. = new Vector.(); + for (var i:int = 0; i < count; i++) { + var joint:Joint = joints[i]; + result[i] = Joint(findClonedJoint(joint, skin, this)); + } + return result; + } + + private function findClonedJoint(joint:Joint, parentSource:Object3D, parentDest:Object3D):Object3D { + for (var srcChild:Object3D = parentSource.childrenList, dstChild:Object3D = parentDest.childrenList; srcChild != null; srcChild = srcChild.next, dstChild = dstChild.next) { + if (srcChild == joint) { + return dstChild; + } + if (srcChild.childrenList != null) { + var j:Object3D = findClonedJoint(joint, srcChild, dstChild); + if (j != null) return j; + } + } + return null; + } + + } +} diff --git a/src/alternativa/engine3d/objects/SkyBox.as b/src/alternativa/engine3d/objects/SkyBox.as new file mode 100644 index 0000000..6188c73 --- /dev/null +++ b/src/alternativa/engine3d/objects/SkyBox.as @@ -0,0 +1,328 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.resources.Geometry; + + use namespace alternativa3d; + + /** + * A polygonal box with faces turned inside. It did not cut on farClipping distance and it is difference with Box. + * + * @see alternativa.engine3d.core.Camera3D#farClipping + */ + public class SkyBox extends Mesh { + + /** + * Left side. + */ + static public const LEFT:String = "left"; + + /** + * Right side. + */ + static public const RIGHT:String = "right"; + + /** + * Back side. + */ + static public const BACK:String = "back"; + + /** + * Front side. + */ + static public const FRONT:String = "front"; + + /** + * Bottom side. + */ + static public const BOTTOM:String = "bottom"; + + /** + * Top side.. + */ + static public const TOP:String = "top"; + + static private var transformProcedureStatic:Procedure = new Procedure([ + // Offset + "sub t0.xyz, i0.xyz, c0.xyz", + // Scale + "mul t0.x, t0.x, c0.w", + "mul t0.y, t0.y, c0.w", + "mul t0.z, t0.z, c0.w", + // Back offset + "add o0.xyz, t0.xyz, c0.xyz", + "mov o0.w, i0.w", + // Declaration + "#c0=cTrans", // Camera position and scale + ]); + + private var leftSurface:Surface; + private var rightSurface:Surface; + private var backSurface:Surface; + private var frontSurface:Surface; + private var bottomSurface:Surface; + private var topSurface:Surface; + + private var size:Number; + + /** + * Creates a new SkyBox instance. + * @param size Length of each edge. + * @param left Material of the left side. + * @param right Material of the right side. + * @param back Material of the back side. + * @param front Material of the front side. + * @param bottom Material of the bottom side. + * @param top Material of the top side. + * @param uvPadding Texture padding in UV space. + * @see alternativa.engine3d.materials.Material + */ + public function SkyBox(size:Number, left:Material = null, right:Material = null, back:Material = null, front:Material = null, bottom:Material = null, top:Material = null, uvPadding:Number = 0) { + + size *= 0.5; + + this.size = size; + + geometry = new Geometry(24); + + var attributes:Array = new Array(); + attributes[0] = VertexAttributes.POSITION; + attributes[1] = VertexAttributes.POSITION; + attributes[2] = VertexAttributes.POSITION; + attributes[6] = VertexAttributes.TEXCOORDS[0]; + attributes[7] = VertexAttributes.TEXCOORDS[0]; + geometry.addVertexStream(attributes); + + geometry.setAttributeValues(VertexAttributes.POSITION, Vector.([ + -size, -size, size, + -size, -size, -size, + -size, size, -size, + -size, size, size, + + size, size, size, + size, size, -size, + size, -size, -size, + size, -size, size, + + size, -size, size, + size, -size, -size, + -size, -size, -size, + -size, -size, size, + + -size, size, size, + -size, size, -size, + size, size, -size, + size, size, size, + + -size, size, -size, + -size, -size, -size, + size, -size, -size, + size, size, -size, + + -size, -size, size, + -size, size, size, + size, size, size, + size, -size, size + ])); + + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[0], Vector.([ + uvPadding, uvPadding, + uvPadding, 1 - uvPadding, + 1 - uvPadding, 1 - uvPadding, + 1 - uvPadding, uvPadding, + + uvPadding, uvPadding, + uvPadding, 1 - uvPadding, + 1 - uvPadding, 1 - uvPadding, + 1 - uvPadding, uvPadding, + + uvPadding, uvPadding, + uvPadding, 1 - uvPadding, + 1 - uvPadding, 1 - uvPadding, + 1 - uvPadding, uvPadding, + + uvPadding, uvPadding, + uvPadding, 1 - uvPadding, + 1 - uvPadding, 1 - uvPadding, + 1 - uvPadding, uvPadding, + + uvPadding, uvPadding, + uvPadding, 1 - uvPadding, + 1 - uvPadding, 1 - uvPadding, + 1 - uvPadding, uvPadding, + + uvPadding, uvPadding, + uvPadding, 1 - uvPadding, + 1 - uvPadding, 1 - uvPadding, + 1 - uvPadding, uvPadding + ])); + + geometry.indices = Vector.([ + 0, 1, 3, 2, 3, 1, + 4, 5, 7, 6, 7, 5, + 8, 9, 11, 10, 11, 9, + 12, 13, 15, 14, 15, 13, + 16, 17, 19, 18, 19, 17, + 20, 21, 23, 22, 23, 21 + ]); + + leftSurface = addSurface(left, 0, 2); + rightSurface = addSurface(right, 6, 2); + backSurface = addSurface(back, 12, 2); + frontSurface = addSurface(front, 18, 2); + bottomSurface = addSurface(bottom, 24, 2); + topSurface = addSurface(top, 30, 2); + + transformProcedure = transformProcedureStatic; + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + for (var i:int = 0; i < _surfacesLength; i++) { + var surface:Surface = _surfaces[i]; + if (surface.material != null) surface.material.collectDraws(camera, surface, geometry, lights, lightsLength, Renderer.SKY); + //Mouse events + if (listening) camera.view.addSurfaceToMouseEvents(surface, geometry, transformProcedure); + } + } + + /** + * @private + */ + override alternativa3d function setTransformConstants(drawUnit:DrawUnit, surface:Surface, vertexShader:Linker, camera:Camera3D):void { + var max:Number = 0; + var dx:Number; + var dy:Number; + var dz:Number; + var len:Number; + dx = -size - cameraToLocalTransform.d; + dy = -size - cameraToLocalTransform.h; + dz = -size - cameraToLocalTransform.l; + len = dx*dx + dy*dy + dz*dz; + if (len > max) max = len; + dx = size - cameraToLocalTransform.d; + dy = -size - cameraToLocalTransform.h; + dz = -size - cameraToLocalTransform.l; + len = dx*dx + dy*dy + dz*dz; + if (len > max) max = len; + dx = size - cameraToLocalTransform.d; + dy = size - cameraToLocalTransform.h; + dz = -size - cameraToLocalTransform.l; + len = dx*dx + dy*dy + dz*dz; + if (len > max) max = len; + dx = -size - cameraToLocalTransform.d; + dy = size - cameraToLocalTransform.h; + dz = -size - cameraToLocalTransform.l; + len = dx*dx + dy*dy + dz*dz; + if (len > max) max = len; + dx = -size - cameraToLocalTransform.d; + dy = -size - cameraToLocalTransform.h; + dz = size - cameraToLocalTransform.l; + len = dx*dx + dy*dy + dz*dz; + if (len > max) max = len; + dx = size - cameraToLocalTransform.d; + dy = -size - cameraToLocalTransform.h; + dz = size - cameraToLocalTransform.l; + len = dx*dx + dy*dy + dz*dz; + if (len > max) max = len; + dx = size - cameraToLocalTransform.d; + dy = size - cameraToLocalTransform.h; + dz = size - cameraToLocalTransform.l; + len = dx*dx + dy*dy + dz*dz; + if (len > max) max = len; + dx = -size - cameraToLocalTransform.d; + dy = size - cameraToLocalTransform.h; + dz = size - cameraToLocalTransform.l; + len = dx*dx + dy*dy + dz*dz; + if (len > max) max = len; + drawUnit.setVertexConstantsFromNumbers(0, cameraToLocalTransform.d, cameraToLocalTransform.h, cameraToLocalTransform.l, camera.farClipping/Math.sqrt(max)); + } + + /** + * Returns a Surface by given alias. You can use SkyBox class constants as value of side parameter. They are following: SkyBox.LEFT, SkyBox.RIGHT, SkyBox.BACK, SkyBox.FRONT, SkyBox.BOTTOM, SkyBox.TOP. + * @param side Surface alias. + * @return Surface by given alias. + */ + public function getSide(side:String):Surface { + switch (side) { + case LEFT: + return leftSurface; + break; + case RIGHT: + return rightSurface; + break; + case BACK: + return backSurface; + break; + case FRONT: + return frontSurface; + break; + case BOTTOM: + return bottomSurface; + break; + case TOP: + return topSurface; + break; + } + return null; + } + + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:SkyBox = new SkyBox(0); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + // Clone marks + var src:SkyBox = source as SkyBox; + for (var i:int = 0; i < src._surfacesLength; i++) { + var surface:Surface = src._surfaces[i]; + var newSurface:Surface = _surfaces[i]; + if (surface == src.leftSurface) { + leftSurface = newSurface; + } else if (surface == src.rightSurface) { + rightSurface = newSurface; + } else if (surface == src.backSurface) { + backSurface = newSurface; + } else if (surface == src.frontSurface) { + frontSurface = newSurface; + } else if (surface == src.bottomSurface) { + bottomSurface = newSurface; + } else if (surface == src.topSurface) { + topSurface = newSurface; + } + } + } + + } +} diff --git a/src/alternativa/engine3d/objects/Sprite3D.as b/src/alternativa/engine3d/objects/Sprite3D.as new file mode 100644 index 0000000..cfd0288 --- /dev/null +++ b/src/alternativa/engine3d/objects/Sprite3D.as @@ -0,0 +1,333 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.resources.Geometry; + + import flash.display3D.Context3D; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * Sprite3D is a flat Object3D always turned in to the camera. + */ + public class Sprite3D extends Object3D { + static private const geometries:Dictionary = new Dictionary(); + + static private var transformProcedureStatic:Procedure = new Procedure([ + // Pivot + "sub t0.z, i0.x, c3.x", + "sub t0.w, i0.y, c3.y", + // Width and height + "mul t0.z, t0.z, c3.z", + "mul t0.w, t0.w, c3.w", + // Rotation + "mov t1.z, c4.w", + "sin t1.x, t1.z", // sin + "cos t1.y, t1.z", // cos + "mul t1.z, t0.z, t1.y", // x*cos + "mul t1.w, t0.w, t1.x", // y*sin + "sub t0.x, t1.z, t1.w", // X + "mul t1.z, t0.z, t1.x", // x*sin + "mul t1.w, t0.w, t1.y", // y*cos + "add t0.y, t1.z, t1.w", // Y + // Offset + "add t0.x, t0.x, c4.x", + "add t0.y, t0.y, c4.y", + "add t0.z, i0.z, c4.z", + "mov t0.w, i0.w", + // Transform to local coordinates + "dp4 o0.x, t0, c0", + "dp4 o0.y, t0, c1", + "dp4 o0.z, t0, c2", + "mov o0.w, t0.w", + // Declaration + "#c0=trans1", + "#c1=trans2", + "#c2=trans3", + "#c3=size", // originX, originY, width, height + "#c4=coords", // x, y, z, rotation + ]); + + static private var deltaTransformProcedureStatic:Procedure = new Procedure([ + // Rotation + "mov t1.z, c4.w", + "sin t1.x, t1.z", // sin + "cos t1.y, t1.z", // cos + "mul t1.z, i0.x, t1.y", // x*cos + "mul t1.w, i0.y, t1.x", // y*sin + "sub t0.x, t1.z, t1.w", // X + "mul t1.z, i0.x, t1.x", // x*sin + "mul t1.w, i0.y, t1.y", // y*cos + "add t0.y, t1.z, t1.w", // Y + "mov t0.z, i0.z", + "mov t0.w, i0.w", + // Transform to local coordinates + "dp3 o0.x, t0, c0", + "dp3 o0.y, t0, c1", + "dp3 o0.z, t0, c2", + // Declaration + "#c0=trans1", + "#c1=trans2", + "#c2=trans3", + "#c3=size", // originX, originY, width, height + "#c4=coords" // x, y, z, rotation + ]); + + /** + * Horizontal coordinate in the Sprite3D plane which defines what part of the plane will placed in x = 0 of the Sprite3D object. The dimension considered with UV-coordinates. + * Thus, if originX = 0, image will drawn from 0 to the right, if originX = -1 – to the left. + * And image will drawn in the center of the Sprite3D, if originX = 0.5. + */ + public var originX:Number = 0.5; + + /** + * Vertical coordinate in the Sprite3D plane which defines what part of the plane will placed in y = 0 of the Sprite3D object. The dimension considered with UV-coordinates. + * Thus, if originY = 0, image will drawn from 0 to the bottom, if originY = -1 – to the top. + * And image will drawn in the center of the Sprite3D, if originY = 0.5. + */ + public var originY:Number = 0.5; + /** + * Rotation in the screen plane, defines in radians. + */ + public var rotation:Number = 0; + + /** + * Width. + */ + public var width:Number; + /** + * Height. + */ + public var height:Number; + + /** + * If true, screen size of a Sprite3D will have perspective correction according to distance to a camera. Otherwise Sprite3D will have fixed size with no dependence on point of view. + */ + public var perspectiveScale:Boolean = true; + + /** + * If true, Sprite3D will drawn over all the rest objects which uses z-buffer sorting. + */ + public var alwaysOnTop:Boolean = false; + + /** + * @private + */ + alternativa3d var surface:Surface; + + /** + * Creates a new Sprite3D instance. + * @param width Width. + * @param height Height + * @param material Material. + * @see alternativa.engine3d.materials.Material + */ + public function Sprite3D(width:Number, height:Number, material:Material = null) { + this.width = width; + this.height = height; + surface = new Surface(); + surface.object = this; + this.material = material; + surface.indexBegin = 0; + surface.numTriangles = 2; + // Transform to the local space + transformProcedure = transformProcedureStatic; + // Transformation of the vector to the local space. + deltaTransformProcedure = deltaTransformProcedureStatic; + } + + /** + * Material of the Sprite3D. + * @see alternativa.engine3d.materials.Material + */ + public function get material():Material { + return surface.material; + } + + /** + * @private + */ + public function set material(value:Material):void { + surface.material = value; + } + + /** + * @private + */ + alternativa3d override function fillResources(resources:Dictionary, hierarchy:Boolean = false, resourceType:Class = null):void { + if (surface.material != null) surface.material.fillResources(resources, resourceType); + super.fillResources(resources, hierarchy, resourceType); + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + var geometry:Geometry = getGeometry(camera.context3D); + if (surface.material != null) surface.material.collectDraws(camera, surface, geometry, lights, lightsLength, alwaysOnTop ? Renderer.NEXT_LAYER : -1); + // Mouse events. + if (listening) camera.view.addSurfaceToMouseEvents(surface, geometry, transformProcedure); + } + + /** + * @private + */ + override alternativa3d function setTransformConstants(drawUnit:DrawUnit, surface:Surface, vertexShader:Linker, camera:Camera3D):void { + // Average size + var scale:Number = Math.sqrt(localToCameraTransform.a*localToCameraTransform.a + localToCameraTransform.e*localToCameraTransform.e + localToCameraTransform.i*localToCameraTransform.i); + scale += Math.sqrt(localToCameraTransform.b*localToCameraTransform.b + localToCameraTransform.f*localToCameraTransform.f + localToCameraTransform.j*localToCameraTransform.j); + scale += Math.sqrt(localToCameraTransform.c*localToCameraTransform.c + localToCameraTransform.g*localToCameraTransform.g + localToCameraTransform.k*localToCameraTransform.k); + scale /= 3; + // Distance dependence + if (!perspectiveScale && !camera.orthographic) scale *= localToCameraTransform.l/camera.focalLength; + // Set the constants + drawUnit.setVertexConstantsFromTransform(0, cameraToLocalTransform); + drawUnit.setVertexConstantsFromNumbers(3, originX, originY, width*scale, height*scale); + drawUnit.setVertexConstantsFromNumbers(4, localToCameraTransform.d, localToCameraTransform.h, localToCameraTransform.l, rotation); + } + + /** + * @private + */ + alternativa3d function getGeometry(context:Context3D):Geometry { + var geometry:Geometry = geometries[context]; + if (geometry == null) { + geometry = new Geometry(4); + + var attributes:Array = new Array(); + attributes[0] = VertexAttributes.POSITION; + attributes[1] = VertexAttributes.POSITION; + attributes[2] = VertexAttributes.POSITION; + attributes[3] = VertexAttributes.NORMAL; + attributes[4] = VertexAttributes.NORMAL; + attributes[5] = VertexAttributes.NORMAL; + attributes[6] = VertexAttributes.TEXCOORDS[0]; + attributes[7] = VertexAttributes.TEXCOORDS[0]; + attributes[8] = VertexAttributes.TEXCOORDS[1]; + attributes[9] = VertexAttributes.TEXCOORDS[1]; + attributes[10] = VertexAttributes.TEXCOORDS[2]; + attributes[11] = VertexAttributes.TEXCOORDS[2]; + attributes[12] = VertexAttributes.TEXCOORDS[3]; + attributes[13] = VertexAttributes.TEXCOORDS[3]; + attributes[14] = VertexAttributes.TEXCOORDS[4]; + attributes[15] = VertexAttributes.TEXCOORDS[4]; + attributes[16] = VertexAttributes.TEXCOORDS[5]; + attributes[17] = VertexAttributes.TEXCOORDS[5]; + attributes[18] = VertexAttributes.TEXCOORDS[6]; + attributes[19] = VertexAttributes.TEXCOORDS[6]; + attributes[20] = VertexAttributes.TEXCOORDS[7]; + attributes[21] = VertexAttributes.TEXCOORDS[7]; + attributes[22] = VertexAttributes.TANGENT4; + attributes[23] = VertexAttributes.TANGENT4; + attributes[24] = VertexAttributes.TANGENT4; + attributes[25] = VertexAttributes.TANGENT4; + geometry.addVertexStream(attributes); + + geometry.setAttributeValues(VertexAttributes.POSITION, Vector.([0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0])); + geometry.setAttributeValues(VertexAttributes.NORMAL, Vector.([0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[0], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[1], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[2], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[3], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[4], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[5], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[6], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[7], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + + geometry.indices = Vector.([0, 1, 3, 2, 3, 1]); + + geometry.upload(context); + geometries[context] = geometry; + } + return geometry; + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:Sprite3D = new Sprite3D(width, height); + res.clonePropertiesFrom(this); + return res; + } + + /** + * @inheritDoc + */ + override protected function clonePropertiesFrom(source:Object3D):void { + super.clonePropertiesFrom(source); + var src:Sprite3D = source as Sprite3D; + width = src.width; + height = src.height; + // autoSize = src.autoSize; + material = src.material; + originX = src.originX; + originY = src.originY; + rotation = src.rotation; + perspectiveScale = src.perspectiveScale; + alwaysOnTop = src.alwaysOnTop; + } + + /** + * @private + */ + override alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + var ww:Number = width; + var hh:Number = height; + // Calculate local radius. + var w:Number = ((originX >= 0.5) ? originX : (1 - originX))*ww; + var h:Number = ((originY >= 0.5) ? originY : (1 - originY))*hh; + var radius:Number = Math.sqrt(w*w + h*h); + var cx:Number = 0; + var cy:Number = 0; + var cz:Number = 0; + if (transform != null) { + // Find average size + var ax:Number = transform.a; + var ay:Number = transform.e; + var az:Number = transform.i; + var size:Number = Math.sqrt(ax*ax + ay*ay + az*az); + ax = transform.b; + ay = transform.f; + az = transform.j; + size += Math.sqrt(ax*ax + ay*ay + az*az); + ax = transform.c; + ay = transform.g; + az = transform.k; + size += Math.sqrt(ax*ax + ay*ay + az*az); + radius *= size/3; + cx = transform.d; + cy = transform.h; + cz = transform.l; + } + if (cx - radius < boundBox.minX) boundBox.minX = cx - radius; + if (cx + radius > boundBox.maxX) boundBox.maxX = cx + radius; + if (cy - radius < boundBox.minY) boundBox.minY = cy - radius; + if (cy + radius > boundBox.maxY) boundBox.maxY = cy + radius; + if (cz - radius < boundBox.minZ) boundBox.minZ = cz - radius; + if (cz + radius > boundBox.maxZ) boundBox.maxZ = cz + radius; + } + } +} diff --git a/src/alternativa/engine3d/objects/Surface.as b/src/alternativa/engine3d/objects/Surface.as new file mode 100644 index 0000000..7d3aef5 --- /dev/null +++ b/src/alternativa/engine3d/objects/Surface.as @@ -0,0 +1,60 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.materials.Material; + + use namespace alternativa3d; + + /** + * Surface is a set of triangles within Mesh object or instance of kindred class like Skin. + * Surface is a entity associated with one material, so different surfaces within one mesh can have different materials. + */ + public class Surface { + + /** + * Material. + */ + public var material:Material; + + /** + * Index of the vertex with which surface starts within index buffer of object's geometry. + * @see alternativa.engine3d.resources.Geometry#indices + */ + public var indexBegin:int = 0; + + /** + * Number of triangles which form this surface. + */ + public var numTriangles:int = 0; + + /** + * @private + */ + alternativa3d var object:Object3D; + + /** + * Returns a copy of this surface. + * @return A copy of this surface. + */ + public function clone():Surface { + var res:Surface = new Surface(); + res.object = object; + res.material = material; + res.indexBegin = indexBegin; + res.numTriangles = numTriangles; + return res; + } + + } +} diff --git a/src/alternativa/engine3d/objects/WireFrame.as b/src/alternativa/engine3d/objects/WireFrame.as new file mode 100644 index 0000000..f108d81 --- /dev/null +++ b/src/alternativa/engine3d/objects/WireFrame.as @@ -0,0 +1,409 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.objects { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.A3DUtils; + import alternativa.engine3d.materials.ShaderProgram; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.resources.Geometry; + import alternativa.engine3d.resources.WireGeometry; + + import flash.display3D.Context3D; + import flash.display3D.Context3DProgramType; + import flash.geom.Vector3D; + import flash.utils.Dictionary; + import flash.utils.getDefinitionByName; + import flash.utils.getQualifiedClassName; + + use namespace alternativa3d; + + /** + * Wireframe is an Object3D which consists of solid lines. Line draws with z-buffer but has no perspective correction, so it has fixed thickness. + * Wireframe can be built on Mesh geometry as well as on sequence of points. + */ + public class WireFrame extends Object3D { + + private static const cachedPrograms:Dictionary = new Dictionary(true); + /** + * @private + */ + alternativa3d var shaderProgram:ShaderProgram; + private var cachedContext3D:Context3D; + + private static function initProgram():ShaderProgram { + var vertexShader:Linker = new Linker(Context3DProgramType.VERTEX); + var transform:Procedure = new Procedure(); + transform.compileFromArray([ + "mov t0, a0", // it is because a0.w holds offset direction + "mov t0.w, c0.y", // replace w with 1 + "m34 t0.xyz, t0, c2", // Transform p0 to the camera coordinates + "m34 t1.xyz, a1, c2", // Transform p1 to the camera coordinates + "sub t2, t1.xyz, t0.xyz", // L = p1 - p0 + // if point places behind the camera, it need to be cut to point lies in the nearClipping plane + "slt t5.x, t0.z, c1.z", // behind = (Q0.z < Camera.near) ? 1 : 0 + "sub t5.y, c0.y, t5.x", // !behind = 1 - behind + //find intersection point of section and nearClipping plane + "add t4.x, t0.z, c0.z", // p0.z + Camera.nearCliping + "sub t4.y, t0.z, t1.z", // p0.z - p1.z + "add t4.y, t4.y, c0.w", // Add some small value for cases of Q0.z = Q1.z + "div t4.z, t4.x, t4.y", // t = ( p0.z - near ) / ( p0.z - p1.z ) + "mul t4.xyz, t4.zzz, t2.xyz", // t(L) + "add t3.xyz, t0.xyz, t4.xyz", // pClipped = p0 + t(L) + // Clip p0 + "mul t0, t0, t5.y", // !behind * p0 + "mul t3.xyz, t3.xyz, t5.x", // behind * pClipped + "add t0, t0, t3.xyz", // newp0 = p0 + pClipped + // Calculate vector of thickness direction + "sub t2, t1.xyz, t0.xyz", // L = p1 - p0 + "crs t3.xyz, t2, t0", // S = L x D + "nrm t3.xyz, t3.xyz", // normalize( S ) + "mul t3.xyz, t3.xyz, a0.w", // Direction correction + "mul t3.xyz, t3.xyz, c1.w", // S *= weight + // Scale vector depends on distance to the camera + "mul t4.x, t0.z, c1.x", // distance *= vpsod + "mul t3.xyz, t3.xyz, t4.xxx", // S.xyz *= pixelScaleFactor + "add t0.xyz, t0.xyz, t3.xyz", // p0 + S + "m44 o0, t0, c5" // projection + ]); + transform.assignVariableName(VariableType.ATTRIBUTE, 0, "pos1"); + transform.assignVariableName(VariableType.ATTRIBUTE, 1, "pos2"); + transform.assignVariableName(VariableType.CONSTANT, 0, "ZERO"); + transform.assignVariableName(VariableType.CONSTANT, 1, "consts"); + transform.assignVariableName(VariableType.CONSTANT, 2, "worldView", 3); + transform.assignVariableName(VariableType.CONSTANT, 5, "proj", 4); + vertexShader.addProcedure(transform); + vertexShader.link(); + + var fragmentShader:Linker = new Linker(Context3DProgramType.FRAGMENT); + var fp:Procedure = new Procedure(); + fp.compileFromArray(["mov o0, c0"]); + fp.assignVariableName(VariableType.CONSTANT, 0, "color"); + fragmentShader.addProcedure(fp); + fragmentShader.link(); + + return new ShaderProgram(vertexShader, fragmentShader); + } + + /** + * Thickness. + */ + public var thickness:Number = 1; + + /** + * @private + */ + alternativa3d var _colorVec:Vector. = new Vector.(4, true); + /** + * @private + */ + alternativa3d var geometry:WireGeometry; + + /** + * The constructor did not make any geometry, so if you need class instance - use static methods createLinesList(), + * createLineStrip() and createEdges() which return one. + * + * @see #createLinesList() + * @see #createLineStrip() + * @see #createEdges() + */ + public function WireFrame(color:uint = 0, alpha:Number = 1, thickness:Number = 0.5) { + this.color = color; + this.alpha = alpha; + this.thickness = thickness; + geometry = new WireGeometry(); + } + + /** + * Transparency. + */ + public function get alpha():Number { + return _colorVec[3]; + } + + /** + * @private + */ + public function set alpha(value:Number):void { + _colorVec[3] = value; + } + + /** + * Color + */ + public function get color():uint { + return (_colorVec[0]*255 << 16) | (_colorVec[1]*255 << 8) | (_colorVec[2]*255); + } + + /** + * @private + */ + public function set color(value:uint):void { + _colorVec[0] = ((value >> 16) & 0xff)/255; + _colorVec[1] = ((value >> 8) & 0xff)/255; + _colorVec[2] = (value & 0xff)/255; + + } + + /** + * @private + */ + override alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + if (geometry != null) { + geometry.updateBoundBox(boundBox, transform); + } + } + + /** + * @private + */ + alternativa3d override function collectDraws(camera:Camera3D, lights:Vector., lightsLength:int):void { + if (camera.context3D != cachedContext3D) { + cachedContext3D = camera.context3D; + shaderProgram = cachedPrograms[cachedContext3D]; + if (shaderProgram == null) { + shaderProgram = initProgram(); + shaderProgram.upload(cachedContext3D); + cachedPrograms[cachedContext3D] = shaderProgram; + } + } + geometry.getDrawUnits(camera, _colorVec, thickness, this, shaderProgram); + } + + /** + * @private + */ + alternativa3d override function fillResources(resources:Dictionary, hierarchy:Boolean = false, resourceType:Class = null):void { + super.fillResources(resources, hierarchy, resourceType); + if (A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(geometry)) as Class, resourceType)) { + resources[geometry] = true; + } + } + + /** + * Creates and returns a new WireFrame instance consists of segments for each couple of points in the given array. + * + * @param points Set of point couples. One point of couple defines start of line segment and another one - end of the line. + * @param color Color of the line. + * @param alpha Transparency. + * @param thickness Thickness. + * @return A new WireFrame instance. + */ + public static function createLinesList(points:Vector., color:uint = 0, alpha:Number = 1, thickness:Number = 1):WireFrame { + var result:WireFrame = new WireFrame(color, alpha, thickness); + var p0:Vector3D; + var p1:Vector3D; + var geometry:WireGeometry = result.geometry; + for (var i:uint = 0, count:uint = points.length - 1; i < count; i += 2) { + p0 = points[i]; + p1 = points[i + 1]; + geometry.addLine(p0.x, p0.y, p0.z, p1.x, p1.y, p1.z); + } + result.calculateBoundBox(); + return result; + } + + /** + * Creates and returns a new WireFrame instance of solid line built on point sequence. + * + * @param points Point sequence. + * @param color Color of the line. + * @param alpha Transparency. + * @param thickness Thickness. + * @return A new WireFrame instance. + */ + public static function createLineStrip(points:Vector., color:uint = 0, alpha:Number = 1, thickness:Number = 1):WireFrame { + var result:WireFrame = new WireFrame(color, alpha, thickness); + var p0:Vector3D; + var p1:Vector3D; + var geometry:WireGeometry = result.geometry; + for (var i:uint = 0, count:uint = points.length - 1; i < count; i++) { + // TODO : don't get vector value twice + p0 = points[i]; + p1 = points[i + 1]; + geometry.addLine(p0.x, p0.y, p0.z, p1.x, p1.y, p1.z); + } + result.calculateBoundBox(); + return result; + } + + /** + * Creates and returns a new WireFrame instance built on edges of given Mesh. + * + * @param mesh Source of geometry. + * @param color Color of the line. + * @param alpha Transparency. + * @param thickness Thickness. + * @return A new WireFrame instance. + */ + public static function createEdges(mesh:Mesh, color:uint = 0, alpha:Number = 1, thickness:Number = 1):WireFrame { + var result:WireFrame = new WireFrame(color, alpha, thickness); + var geometry:Geometry = mesh.geometry; + var resultGeometry:WireGeometry = result.geometry; + var edges:Dictionary = new Dictionary(); + var indices:Vector. = geometry.indices; + var vertices:Vector. = geometry.getAttributeValues(VertexAttributes.POSITION); + // Loop over all the faces of mesh, create lines like 0-1-2-0 + for (var i:int = 0, count:int = indices.length; i < count; i += 3) { + var index:uint = indices[i]*3; + var v1x:Number = vertices[index]; + index++; + var v1y:Number = vertices[index]; + index++; + var v1z:Number = vertices[index]; + index = indices[int(i + 1)]*3; + var v2x:Number = vertices[index]; + index++; + var v2y:Number = vertices[index]; + index++; + var v2z:Number = vertices[index]; + index = indices[int(i + 2)]*3; + var v3x:Number = vertices[index]; + index++; + var v3y:Number = vertices[index]; + index++; + var v3z:Number = vertices[index]; + if (checkEdge(edges, v1x, v1y, v1z, v2x, v2y, v2z)) { + resultGeometry.addLine(v1x, v1y, v1z, v2x, v2y, v2z); + } + if (checkEdge(edges, v2x, v2y, v2z, v3x, v3y, v3z)) { + resultGeometry.addLine(v2x, v2y, v2z, v3x, v3y, v3z); + } + if (checkEdge(edges, v1x, v1y, v1z, v3x, v3y, v3z)) { + resultGeometry.addLine(v1x, v1y, v1z, v3x, v3y, v3z); + } + } + result.calculateBoundBox(); + result._x = mesh._x; + result._y = mesh._y; + result._z = mesh._z; + result._rotationX = mesh._rotationX; + result._rotationY = mesh._rotationY; + result._rotationZ = mesh._rotationZ; + result._scaleX = mesh._scaleX; + result._scaleY = mesh._scaleY; + result._scaleZ = mesh._scaleZ; + return result; + } + + alternativa3d static function createNormals(mesh:Mesh, color:uint = 0, alpha:Number = 1, thickness:Number = 1, length:Number = 1):WireFrame { + var result:WireFrame = new WireFrame(color, alpha, thickness); + var geometry:Geometry = mesh.geometry; + var resultGeometry:WireGeometry = result.geometry; + var vertices:Vector. = geometry.getAttributeValues(VertexAttributes.POSITION); + var normals:Vector. = geometry.getAttributeValues(VertexAttributes.NORMAL); + var numVertices:uint = geometry._numVertices; + for (var i:int = 0; i < numVertices; i++) { + var index:uint = i*3; + resultGeometry.addLine( + vertices[index], vertices[int(index + 1)], vertices[int(index + 2)], + vertices[index] + normals[index]*length, vertices[int(index + 1)] + normals[int(index + 1)]*length, vertices[int(index + 2)] + normals[int(index + 2)]*length); + } + result.calculateBoundBox(); + result._x = mesh._x; + result._y = mesh._y; + result._z = mesh._z; + result._rotationX = mesh._rotationX; + result._rotationY = mesh._rotationY; + result._rotationZ = mesh._rotationZ; + result._scaleX = mesh._scaleX; + result._scaleY = mesh._scaleY; + result._scaleZ = mesh._scaleZ; + return result; + } + + /** + * @private + */ + alternativa3d static function createTangents(mesh:Mesh, color:uint = 0, alpha:Number = 1, thickness:Number = 1, length:Number = 1):WireFrame { + var result:WireFrame = new WireFrame(color, alpha, thickness); + var geometry:Geometry = mesh.geometry; + var resultGeometry:WireGeometry = result.geometry; + var vertices:Vector. = geometry.getAttributeValues(VertexAttributes.POSITION); + var tangents:Vector. = geometry.getAttributeValues(VertexAttributes.TANGENT4); + var numVertices:uint = geometry._numVertices; + for (var i:int = 0; i < numVertices; i++) { + var index:uint = i*3; + resultGeometry.addLine( + vertices[index], vertices[int(index + 1)], vertices[int(index + 2)], + vertices[index] + tangents[int(i*4)]*length, vertices[int(index + 1)] + tangents[int(i*4 + 1)]*length, vertices[int(index + 2)] + tangents[int(i*4 + 2)]*length); + } + result.calculateBoundBox(); + result._x = mesh._x; + result._y = mesh._y; + result._z = mesh._z; + result._rotationX = mesh._rotationX; + result._rotationY = mesh._rotationY; + result._rotationZ = mesh._rotationZ; + result._scaleX = mesh._scaleX; + result._scaleY = mesh._scaleY; + result._scaleZ = mesh._scaleZ; + return result; + } + + /** + * @private + */ + alternativa3d static function createBinormals(mesh:Mesh, color:uint = 0, alpha:Number = 1, thickness:Number = 1, length:Number = 1):WireFrame { + var result:WireFrame = new WireFrame(color, alpha, thickness); + var geometry:Geometry = mesh.geometry; + var resultGeometry:WireGeometry = result.geometry; + var vertices:Vector. = geometry.getAttributeValues(VertexAttributes.POSITION); + var tangents:Vector. = geometry.getAttributeValues(VertexAttributes.TANGENT4); + var normals:Vector. = geometry.getAttributeValues(VertexAttributes.NORMAL); + var numVertices:uint = geometry._numVertices; + for (var i:int = 0; i < numVertices; i++) { + var index:uint = i*3; + var normal:Vector3D = new Vector3D(normals[index], normals[int(index + 1)], normals[int(index + 2)]); + var tangent:Vector3D = new Vector3D(tangents[int(i*4)], tangents[int(i*4 + 1)], tangents[int(i*4 + 2)]); + var binormal:Vector3D = normal.crossProduct(tangent); + + binormal.scaleBy(tangents[int(i*4 + 3)]); + binormal.normalize(); + resultGeometry.addLine( + vertices[index], vertices[int(index + 1)], vertices[int(index + 2)], + vertices[index] + binormal.z*length, vertices[int(index + 1)] + binormal.z*length, vertices[int(index + 2)] + binormal.z*length); + } + result.calculateBoundBox(); + result._x = mesh._x; + result._y = mesh._y; + result._z = mesh._z; + result._rotationX = mesh._rotationX; + result._rotationY = mesh._rotationY; + result._rotationZ = mesh._rotationZ; + result._scaleX = mesh._scaleX; + result._scaleY = mesh._scaleY; + result._scaleZ = mesh._scaleZ; + return result; + } + + private static function checkEdge(edges:Dictionary, v1x:Number, v1y:Number, v1z:Number, v2x:Number, v2y:Number, v2z:Number):Boolean { + var str:String; + if (v1x*v1x + v1y*v1y + v1z*v1z < v2x*v2x + v2y*v2y + v2z*v2z) { + str = v1x.toString() + v1y.toString() + v1z.toString() + v2x.toString() + v2y.toString() + v2z.toString(); + } else { + str = v2x.toString() + v2y.toString() + v2z.toString() + v1x.toString() + v1y.toString() + v1z.toString(); + } + if (edges[str]) return false; + edges[str] = true; + return true; + } + + } +} diff --git a/src/alternativa/engine3d/primitives/Box.as b/src/alternativa/engine3d/primitives/Box.as new file mode 100644 index 0000000..a0e7119 --- /dev/null +++ b/src/alternativa/engine3d/primitives/Box.as @@ -0,0 +1,283 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.primitives { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.resources.Geometry; + + import flash.utils.ByteArray; + import flash.utils.Endian; + + use namespace alternativa3d; + + /** + * A cuboid primitive. + */ + public class Box extends Mesh { + /** + * Creates a new Box instance. + * @param width Width. Can not be less than 0. + * @param length Length. Can not be less than 0. + * @param height Height. Can not be less than 0. + * @param widthSegments Number of subdivisions along x-axis. + * @param lengthSegments Number of subdivisions along y-axis. + * @param heightSegments Number of subdivisions along z-axis. + * @param reverse If true, face normals will turned inside, so the box will be visible from inside only. Otherwise, the normals will turned outside. + * @param material Material. + */ + public function Box(width:Number = 100, length:Number = 100, height:Number = 100, widthSegments:uint = 1, lengthSegments:uint = 1, heightSegments:uint = 1, reverse:Boolean = false, material:Material = null) { + if (widthSegments <= 0 || lengthSegments <= 0 || heightSegments <= 0) return; + var indices:Vector. = new Vector.(); + var x:int; + var y:int; + var z:int; + var wp:int = widthSegments + 1; + var lp:int = lengthSegments + 1; + var hp:int = heightSegments + 1; + var halfWidth:Number = width*0.5; + var halfLength:Number = length*0.5; + var halfHeight:Number = height*0.5; + var wd:Number = 1/widthSegments; + var ld:Number = 1/lengthSegments; + var hd:Number = 1/heightSegments; + var ws:Number = width/widthSegments; + var ls:Number = length/lengthSegments; + var hs:Number = height/heightSegments; + + var vertices:ByteArray = new ByteArray(); + vertices.endian = Endian.LITTLE_ENDIAN; + var offset:uint = 0; + var offsetFromPos:Number = 28; + // Bottom face + for (x = 0; x < wp; x++) { + for (y = 0; y < lp; y++) { + vertices.writeFloat(x*ws - halfWidth); + vertices.writeFloat(y*ls - halfLength); + vertices.writeFloat(-halfHeight); + vertices.writeFloat((widthSegments - x)*wd); + vertices.writeFloat((lengthSegments - y)*ld); + vertices.length = vertices.position += offsetFromPos; + } + } + offset = vertices.position; + for (x = 0; x < wp; x++) { + for (y = 0; y < lp; y++) { + if (x < widthSegments && y < lengthSegments) { + createFace(indices, vertices, (x + 1)*lp + y + 1, (x + 1)*lp + y, x*lp + y, x*lp + y + 1, 0, 0, -1, halfHeight, 0,-1,0,0, reverse); + } + } + } + vertices.position = offset; + var o:uint = wp*lp; + // Top face + for (x = 0; x < wp; x++) { + for (y = 0; y < lp; y++) { + vertices.writeFloat(x*ws - halfWidth); + vertices.writeFloat(y*ls - halfLength); + vertices.writeFloat(halfHeight); + vertices.writeFloat(x*wd); + vertices.writeFloat((lengthSegments - y)*ld); + vertices.length = vertices.position += offsetFromPos; + } + } + offset = vertices.position; + for (x = 0; x < wp; x++) { + for (y = 0; y < lp; y++) { + if (x < widthSegments && y < lengthSegments) { + createFace(indices, vertices, o + x*lp + y, o + (x + 1)*lp + y, o + (x + 1)*lp + y + 1, o + x*lp + y + 1, 0, 0, 1, halfHeight,0,-1,0,0, reverse); + } + } + } + vertices.position = offset; + o += wp*lp; + // Back face + for (x = 0; x < wp; x++) { + for (z = 0; z < hp; z++) { + vertices.writeFloat(x*ws - halfWidth); + vertices.writeFloat(-halfLength); + vertices.writeFloat(z*hs - halfHeight); + vertices.writeFloat(x*wd); + vertices.writeFloat((heightSegments - z)*hd); + vertices.length = vertices.position += offsetFromPos; + } + } + + offset = vertices.position; + for (x = 0; x < wp; x++) { + for (z = 0; z < hp; z++) { + if (x < widthSegments && z < heightSegments) { + createFace(indices, vertices, o + x*hp + z, o + (x + 1)*hp + z, o + (x + 1)*hp + z + 1, o + x*hp + z + 1, 0, -1, 0, halfLength,0,0,-1,0, reverse); + } + } + } + vertices.position = offset; + o += wp*hp; + // Front face + for (x = 0; x < wp; x++) { + for (z = 0; z < hp; z++) { + vertices.writeFloat(x*ws - halfWidth); + vertices.writeFloat(halfLength); + vertices.writeFloat(z*hs - halfHeight); + vertices.writeFloat((widthSegments - x)*wd); + vertices.writeFloat((heightSegments - z)*hd); + vertices.length = vertices.position += offsetFromPos; + } + } + offset = vertices.position; + for (x = 0; x < wp; x++) { + for (z = 0; z < hp; z++) { + if (x < widthSegments && z < heightSegments) { + createFace(indices, vertices, o + x*hp + z, o + x*hp + z + 1, o + (x + 1)*hp + z + 1, o + (x + 1)*hp + z, 0, 1, 0, halfLength,0,0,-1,0, reverse); + } + } + } + vertices.position = offset; + o += wp*hp; + // Left face + for (y = 0; y < lp; y++) { + for (z = 0; z < hp; z++) { + vertices.writeFloat(-halfWidth); + vertices.writeFloat(y*ls - halfLength); + vertices.writeFloat(z*hs - halfHeight); + vertices.writeFloat((lengthSegments - y)*ld); + vertices.writeFloat((heightSegments - z)*hd); + vertices.length = vertices.position += offsetFromPos; + } + } + offset = vertices.position; + for (y = 0; y < lp; y++) { + for (z = 0; z < hp; z++) { + if (y < lengthSegments && z < heightSegments) { + createFace(indices, vertices, o + y*hp + z, o + y*hp + z + 1, o + (y + 1)*hp + z + 1, o + (y + 1)*hp + z, -1, 0, 0, halfWidth, 0,0,-1,0,reverse); + } + } + } + vertices.position = offset; + o += lp*hp; + // Right face + for (y = 0; y < lp; y++) { + for (z = 0; z < hp; z++) { + vertices.writeFloat(halfWidth); + vertices.writeFloat(y*ls - halfLength); + vertices.writeFloat(z*hs - halfHeight); + vertices.writeFloat(y*ld); + vertices.writeFloat((heightSegments - z)*hd); + vertices.length = vertices.position += offsetFromPos; + } + } + for (y = 0; y < lp; y++) { + for (z = 0; z < hp; z++) { + if (y < lengthSegments && z < heightSegments) { + createFace(indices, vertices, o + y*hp + z, o + (y + 1)*hp + z, o + (y + 1)*hp + z + 1, o + y*hp + z + 1, 1, 0, 0, halfWidth,0,0,-1,0, reverse); + } + } + } + + // Set bounds + geometry = new Geometry(); + geometry._indices = indices; + var attributes:Array = new Array; + attributes[0] = VertexAttributes.POSITION; + attributes[1] = VertexAttributes.POSITION; + attributes[2] = VertexAttributes.POSITION; + attributes[3] = VertexAttributes.TEXCOORDS[0]; + attributes[4] = VertexAttributes.TEXCOORDS[0]; + attributes[5] = VertexAttributes.NORMAL; + attributes[6] = VertexAttributes.NORMAL; + attributes[7] = VertexAttributes.NORMAL; + attributes[8] = VertexAttributes.TANGENT4; + attributes[9] = VertexAttributes.TANGENT4; + attributes[10] = VertexAttributes.TANGENT4; + attributes[11] = VertexAttributes.TANGENT4; + + geometry.addVertexStream(attributes); + geometry._vertexStreams[0].data = vertices; + geometry._numVertices = vertices.length/48; + addSurface(material, 0, indices.length/3); + + boundBox = new BoundBox(); + boundBox.minX = -halfWidth; + boundBox.minY = -halfLength; + boundBox.minZ = -halfHeight; + boundBox.maxX = halfWidth; + boundBox.maxY = halfLength; + boundBox.maxZ = halfHeight; + } + + private function createFace(indices:Vector., vertices:ByteArray, a:int, b:int, c:int, d:int, nx:Number, ny:Number, nz:Number, no:Number, tx:Number, ty:Number, tz:Number, tw:Number, reverse:Boolean):void { + var v:int; + if (reverse) { + nx = -nx; + ny = -ny; + nz = -nz; + no = -no; + v = a; + a = d; + d = v; + v = b; + b = c; + c = v; + } + indices.push(a); + indices.push(b); + indices.push(c); + indices.push(a); + indices.push(c); + indices.push(d); + vertices.position = a*48 + 20; + vertices.writeFloat(nx); + vertices.writeFloat(ny); + vertices.writeFloat(nz); + vertices.writeFloat(tx); + vertices.writeFloat(ty); + vertices.writeFloat(tz); + vertices.writeFloat(tw); + vertices.position = b*48 + 20; + vertices.writeFloat(nx); + vertices.writeFloat(ny); + vertices.writeFloat(nz); + vertices.writeFloat(tx); + vertices.writeFloat(ty); + vertices.writeFloat(tz); + vertices.writeFloat(tw); + vertices.position = c*48 + 20; + vertices.writeFloat(nx); + vertices.writeFloat(ny); + vertices.writeFloat(nz); + vertices.writeFloat(tx); + vertices.writeFloat(ty); + vertices.writeFloat(tz); + vertices.writeFloat(tw); + vertices.position = d*48 + 20; + vertices.writeFloat(nx); + vertices.writeFloat(ny); + vertices.writeFloat(nz); + vertices.writeFloat(tx); + vertices.writeFloat(ty); + vertices.writeFloat(tz); + vertices.writeFloat(tw); + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:Box = new Box(0, 0, 0, 0, 0, 0); + res.clonePropertiesFrom(this); + return res; + } + } +} diff --git a/src/alternativa/engine3d/primitives/GeoSphere.as b/src/alternativa/engine3d/primitives/GeoSphere.as new file mode 100644 index 0000000..0a2a361 --- /dev/null +++ b/src/alternativa/engine3d/primitives/GeoSphere.as @@ -0,0 +1,407 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.primitives { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.resources.Geometry; + + import flash.geom.Vector3D; + import flash.utils.ByteArray; + import flash.utils.Endian; + + use namespace alternativa3d; + + /** + * A spherical primitive consists of set of equal triangles. + */ + public class GeoSphere extends Mesh { + + /** + * Creates a new GeoSphere instance. + * + * @param radius Radius of a sphere. Can't be less than 0. + * @param segments Level of subdivision. + * @param reverse If true, face normals will turned inside, so the sphere will be visible from inside only. Otherwise, the normals will turned outside. + * @param material Material. If you use TextureMaterial, it is need to set repeat property to true. + */ + public function GeoSphere(radius:Number = 100, segments:uint = 2, reverse:Boolean = false, material:Material = null) { + if (segments == 0) return; + radius = (radius < 0) ? 0 : radius; + var indices:Vector. = new Vector.(); + var sections:uint = 20; + var deg180:Number = Math.PI; + var deg360:Number = Math.PI*2; + var vertices:Vector. = new Vector.(); + var uvs:Vector. = new Vector.(); + var i:uint; + var f:uint; + var theta:Number; + var sin:Number; + var cos:Number; + // distance along z-axis to the top and bottom pole hats + var subz:Number = 4.472136E-001*radius; + //radius along subz distance. + var subrad:Number = 2*subz; + vertices.push(new Vector3D(0, 0, radius, -1)); + uvs.length += 2; + // Make vertices of the top pole hats + for (i = 0; i < 5; i++) { + theta = deg360*i/5; + sin = Math.sin(theta); + cos = Math.cos(theta); + vertices.push(new Vector3D(subrad*cos, subrad*sin, subz, -1)); + uvs.length += 2; + } + // Make vertices of the bottom pole hats + for (i = 0; i < 5; i++) { + theta = deg180*((i << 1) + 1)/5; + sin = Math.sin(theta); + cos = Math.cos(theta); + vertices.push(new Vector3D(subrad*cos, subrad*sin, -subz, -1)); + uvs.length += 2; + } + vertices.push(new Vector3D(0, 0, -radius, -1)); + uvs.length += 2; + for (i = 1; i < 6; i++) { + interpolate(0, i, segments, vertices, uvs); + + } + for (i = 1; i < 6; i++) { + interpolate(i, i%5 + 1, segments, vertices, uvs); + } + for (i = 1; i < 6; i++) { + interpolate(i, i + 5, segments, vertices, uvs); + } + for (i = 1; i < 6; i++) { + interpolate(i, (i + 3)%5 + 6, segments, vertices, uvs); + } + for (i = 1; i < 6; i++) { + interpolate(i + 5, i%5 + 6, segments, vertices, uvs); + } + for (i = 6; i < 11; i++) { + interpolate(11, i, segments, vertices, uvs); + } + for (f = 0; f < 5; f++) { + for (i = 1; i <= segments - 2; i++) { + interpolate(12 + f*(segments - 1) + i, 12 + (f + 1)%5*(segments - 1) + i, i + 1, vertices, uvs); + } + } + for (f = 0; f < 5; f++) { + for (i = 1; i <= segments - 2; i++) { + interpolate(12 + (f + 15)*(segments - 1) + i, 12 + (f + 10)*(segments - 1) + i, i + 1, vertices, uvs); + } + } + for (f = 0; f < 5; f++) { + for (i = 1; i <= segments - 2; i++) { + interpolate(12 + ((f + 1)%5 + 15)*(segments - 1) + segments - 2 - i, 12 + (f + 10)*(segments - 1) + segments - 2 - i, i + 1, vertices, uvs); + } + } + for (f = 0; f < 5; f++) { + for (i = 1; i <= segments - 2; i++) { + interpolate(12 + ((f + 1)%5 + 25)*(segments - 1) + i, 12 + (f + 25)*(segments - 1) + i, i + 1, vertices, uvs); + } + } + // Make faces + for (f = 0; f < sections; f++) { + for (var row:uint = 0; row < segments; row++) { + for (var column:uint = 0; column <= row; column++) { + var aIndex:uint = findVertices(segments, f, row, column); + var bIndex:uint = findVertices(segments, f, row + 1, column); + var cIndex:uint = findVertices(segments, f, row + 1, column + 1); + var a:Vector3D = vertices[aIndex]; + var b:Vector3D = vertices[bIndex]; + var c:Vector3D = vertices[cIndex]; + var au:Number; + var av:Number; + var bu:Number; + var bv:Number; + var cu:Number; + var cv:Number; + if (a.y >= 0 && (a.x < 0) && (b.y < 0 || c.y < 0)) { + au = Math.atan2(a.y, a.x)/deg360 - 0.5; + } else { + au = Math.atan2(a.y, a.x)/deg360 + 0.5; + } + av = -Math.asin(a.z/radius)/deg180 + 0.5; + if (b.y >= 0 && (b.x < 0) && (a.y < 0 || c.y < 0)) { + bu = Math.atan2(b.y, b.x)/deg360 - 0.5; + } else { + bu = Math.atan2(b.y, b.x)/deg360 + 0.5; + } + bv = -Math.asin(b.z/radius)/deg180 + 0.5; + if (c.y >= 0 && (c.x < 0) && (a.y < 0 || b.y < 0)) { + cu = Math.atan2(c.y, c.x)/deg360 - 0.5; + } else { + cu = Math.atan2(c.y, c.x)/deg360 + 0.5; + } + cv = -Math.asin(c.z/radius)/deg180 + 0.5; + // Pole + if (aIndex == 0 || aIndex == 11) { + au = bu + (cu - bu)*0.5; + } + if (bIndex == 0 || bIndex == 11) { + bu = au + (cu - au)*0.5; + } + if (cIndex == 0 || cIndex == 11) { + cu = au + (bu - au)*0.5; + } + // Duplication + if (a.w > 0 && uvs[aIndex*2] != au) { + a = createVertex(a.x, a.y, a.z); + aIndex = vertices.push(a) - 1; + + } + uvs[aIndex*2] = au; + uvs[aIndex*2 + 1] = av; + a.w = 1; + if (b.w > 0 && uvs[bIndex*2] != bu) { + b = createVertex(b.x, b.y, b.z); + bIndex = vertices.push(b) - 1; + } + uvs[bIndex*2] = bu; + uvs[bIndex*2 + 1] = bv; + b.w = 1; + if (c.w > 0 && uvs[cIndex*2] != cu) { + c = createVertex(c.x, c.y, c.z); + cIndex = vertices.push(c) - 1; + } + uvs[cIndex*2] = cu; + uvs[cIndex*2 + 1] = cv; + c.w = 1; + if (reverse) { + indices.push(aIndex, cIndex, bIndex); + } else { + indices.push(aIndex, bIndex, cIndex); + } + if (column < row) { + bIndex = findVertices(segments, f, row, column + 1); + b = vertices[bIndex]; + if (a.y >= 0 && (a.x < 0) && (b.y < 0 || c.y < 0)) { + au = Math.atan2(a.y, a.x)/deg360 - 0.5; + } else { + au = Math.atan2(a.y, a.x)/deg360 + 0.5; + } + av = -Math.asin(a.z/radius)/deg180 + 0.5; + if (b.y >= 0 && (b.x < 0) && (a.y < 0 || c.y < 0)) { + bu = Math.atan2(b.y, b.x)/deg360 - 0.5; + } else { + bu = Math.atan2(b.y, b.x)/deg360 + 0.5; + } + bv = -Math.asin(b.z/radius)/deg180 + 0.5; + if (c.y >= 0 && (c.x < 0) && (a.y < 0 || b.y < 0)) { + cu = Math.atan2(c.y, c.x)/deg360 - 0.5; + } else { + cu = Math.atan2(c.y, c.x)/deg360 + 0.5; + } + cv = -Math.asin(c.z/radius)/deg180 + 0.5; + if (aIndex == 0 || aIndex == 11) { + au = bu + (cu - bu)*0.5; + } + if (bIndex == 0 || bIndex == 11) { + bu = au + (cu - au)*0.5; + } + if (cIndex == 0 || cIndex == 11) { + cu = au + (bu - au)*0.5; + } + // Duplication + if (a.w > 0 && uvs[aIndex*2] != au) { + a = createVertex(a.x, a.y, a.z); + aIndex = vertices.push(a) - 1; + } + uvs[aIndex*2] = au; + uvs[aIndex*2 + 1] = av; + a.w = 1; + if (b.w > 0 && uvs[bIndex*2] != bu) { + b = createVertex(b.x, b.y, b.z); + bIndex = vertices.push(b) - 1; + } + uvs[bIndex*2] = bu; + uvs[bIndex*2 + 1] = bv; + b.w = 1; + if (c.w > 0 && uvs[cIndex*2] != cu) { + c = createVertex(c.x, c.y, c.z); + cIndex = vertices.push(c) - 1; + } + uvs[cIndex*2] = cu; + uvs[cIndex*2 + 1] = cv; + c.w = 1; + if (reverse) { + indices.push(aIndex, bIndex, cIndex); + } else { + indices.push(aIndex, cIndex, bIndex); + } + } + } + } + } + + var byteArray:ByteArray = new ByteArray(); + byteArray.endian = Endian.LITTLE_ENDIAN; + for (i = 0; i < vertices.length; i++) { + var v:Vector3D = vertices[i]; + byteArray.writeFloat(v.x); + byteArray.writeFloat(v.y); + byteArray.writeFloat(v.z); + byteArray.writeFloat(uvs[i*2]); + byteArray.writeFloat(uvs[i*2 + 1]); + byteArray.writeFloat(v.x / radius); + byteArray.writeFloat(v.y / radius); + byteArray.writeFloat(v.z / radius); + + var longitude:Number = deg360 * uvs[i*2]; + byteArray.writeFloat( +Math.sin (longitude)); + byteArray.writeFloat( -Math.cos (longitude)); + byteArray.writeFloat(0.0); + byteArray.writeFloat(-1.0); + } + + geometry = new Geometry(); + geometry._indices = indices; + var attributes:Array = new Array(); + attributes[0] = VertexAttributes.POSITION; + attributes[1] = VertexAttributes.POSITION; + attributes[2] = VertexAttributes.POSITION; + attributes[3] = VertexAttributes.TEXCOORDS[0]; + attributes[4] = VertexAttributes.TEXCOORDS[0]; + attributes[5] = VertexAttributes.NORMAL; + attributes[6] = VertexAttributes.NORMAL; + attributes[7] = VertexAttributes.NORMAL; + attributes[8] = VertexAttributes.TANGENT4; + attributes[9] = VertexAttributes.TANGENT4; + attributes[10] = VertexAttributes.TANGENT4; + attributes[11] = VertexAttributes.TANGENT4; + + geometry.addVertexStream(attributes); + geometry._vertexStreams[0].data = byteArray; + geometry._numVertices = byteArray.length/48; + +// this.geometry.calculateFacesNormals(); + addSurface(material, 0, indices.length/3); + calculateBoundBox(); + } + + private function createVertex(x:Number, y:Number, z:Number):Vector3D { + var vertex:Vector3D = new Vector3D(); + vertex.x = x; + vertex.y = y; + vertex.z = z; + vertex.w = -1; + return vertex; + } + + private function interpolate(v1:uint, v2:uint, num:uint, vertices:Vector., uvs:Vector.):void { + if (num < 2) { + return; + } + var a:Vector3D = vertices[v1]; + var b:Vector3D = vertices[v2]; + var cos:Number = (a.x*b.x + a.y*b.y + a.z*b.z)/(a.x*a.x + a.y*a.y + a.z*a.z); + cos = (cos < -1) ? -1 : ((cos > 1) ? 1 : cos); + var theta:Number = Math.acos(cos); + var sin:Number = Math.sin(theta); + for (var e:uint = 1; e < num; e++) { + var theta1:Number = theta*e/num; + var theta2:Number = theta*(num - e)/num; + var st1:Number = Math.sin(theta1); + var st2:Number = Math.sin(theta2); + vertices.push(new Vector3D((a.x*st2 + b.x*st1)/sin, (a.y*st2 + b.y*st1)/sin, (a.z*st2 + b.z*st1)/sin, -1)); + uvs.length += 2; + } + } + + private function findVertices(segments:uint, section:uint, row:uint, column:uint):uint { + if (row == 0) { + if (section < 5) { + return (0); + } + if (section > 14) { + return (11); + } + return (section - 4); + } + if (row == segments && column == 0) { + if (section < 5) { + return (section + 1); + } + if (section < 10) { + return ((section + 4)%5 + 6); + } + if (section < 15) { + return ((section + 1)%5 + 1); + } + return ((section + 1)%5 + 6); + } + if (row == segments && column == segments) { + if (section < 5) { + return ((section + 1)%5 + 1); + } + if (section < 10) { + return (section + 1); + } + if (section < 15) { + return (section - 9); + } + return (section - 9); + } + if (row == segments) { + if (section < 5) { + return (12 + (5 + section)*(segments - 1) + column - 1); + } + if (section < 10) { + return (12 + (20 + (section + 4)%5)*(segments - 1) + column - 1); + } + if (section < 15) { + return (12 + (section - 5)*(segments - 1) + segments - 1 - column); + } + return (12 + (5 + section)*(segments - 1) + segments - 1 - column); + } + if (column == 0) { + if (section < 5) { + return (12 + section*(segments - 1) + row - 1); + } + if (section < 10) { + return (12 + (section%5 + 15)*(segments - 1) + row - 1); + } + if (section < 15) { + return (12 + ((section + 1)%5 + 15)*(segments - 1) + segments - 1 - row); + } + return (12 + ((section + 1)%5 + 25)*(segments - 1) + row - 1); + } + if (column == row) { + if (section < 5) { + return (12 + (section + 1)%5*(segments - 1) + row - 1); + } + if (section < 10) { + return (12 + (section%5 + 10)*(segments - 1) + row - 1); + } + if (section < 15) { + return (12 + (section%5 + 10)*(segments - 1) + segments - row - 1); + } + return (12 + (section%5 + 25)*(segments - 1) + row - 1); + } + return (12 + 30*(segments - 1) + section*(segments - 1)*(segments - 2)/2 + (row - 1)*(row - 2)/2 + column - 1); + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:GeoSphere = new GeoSphere(1, 0); + res.clonePropertiesFrom(this); + return res; + } + } + +} diff --git a/src/alternativa/engine3d/primitives/Plane.as b/src/alternativa/engine3d/primitives/Plane.as new file mode 100644 index 0000000..6830ae7 --- /dev/null +++ b/src/alternativa/engine3d/primitives/Plane.as @@ -0,0 +1,202 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.primitives { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.resources.Geometry; + + import flash.utils.ByteArray; + import flash.utils.Endian; + + use namespace alternativa3d; + + /** + * A plane primitive. + */ + public class Plane extends Mesh { + + /** + * Creates a new Plane instance. + * @param width Width. Can not be less than 0. + * @param length Length. Can not be less than 0. + * @param widthSegments Number of subdivisions along x-axis. + * @param lengthSegments Number of subdivisions along y-axis. + * @param twoSided If true, plane has surface for both sides: tob and bottom and only one otherwise. + * @param reverse If twoSided=false, reverse parameter determines for which side surface will be created. + * @param bottom Material of the bottom surface. + * @param top Material of the top surface. + */ + public function Plane(width:Number = 100, length:Number = 100, widthSegments:uint = 1, lengthSegments:uint = 1, twoSided:Boolean = true, reverse:Boolean = false, bottom:Material = null, top:Material = null) { + if (widthSegments <= 0 || lengthSegments <= 0) return; + var indices:Vector. = new Vector.(); + var x:int; + var y:int; + var wEdges:int = widthSegments + 1; + var lEdges:int = lengthSegments + 1; + var halfWidth:Number = width*0.5; + var halfLength:Number = length*0.5; + var segmentUSize:Number = 1/widthSegments; + var segmentVSize:Number = 1/lengthSegments; + var segmentWidth:Number = width/widthSegments; + var segmentLength:Number = length/lengthSegments; + + var vertices:ByteArray = new ByteArray(); + vertices.endian = Endian.LITTLE_ENDIAN; + var offsetAdditionalData:Number = 28; + // Top face. + for (x = 0; x < wEdges; x++) { + for (y = 0; y < lEdges; y++) { + vertices.writeFloat(x*segmentWidth - halfWidth); + vertices.writeFloat(y*segmentLength - halfLength); + vertices.writeFloat(0); + vertices.writeFloat(x*segmentUSize); + vertices.writeFloat((lengthSegments - y)*segmentVSize); + vertices.length = vertices.position += offsetAdditionalData; + } + } + var lastPosition:uint = vertices.position; + for (x = 0; x < wEdges; x++) { + for (y = 0; y < lEdges; y++) { + if (x < widthSegments && y < lengthSegments) { + createFace(indices, vertices, x*lEdges + y, (x + 1)*lEdges + y, (x + 1)*lEdges + y + 1, x*lEdges + y + 1, 0, 0, 1, 1, 0, 0, -1, reverse); + } + } + } + + if (twoSided) { + vertices.position = lastPosition; + // Bottom face. + for (x = 0; x < wEdges; x++) { + for (y = 0; y < lEdges; y++) { + vertices.writeFloat(x*segmentWidth - halfWidth); + vertices.writeFloat(y*segmentLength - halfLength); + vertices.writeFloat(0); + vertices.writeFloat((widthSegments - x)*segmentUSize); + vertices.writeFloat((lengthSegments - y)*segmentVSize); + vertices.length = vertices.position += offsetAdditionalData; + } + } + var baseIndex:uint = wEdges*lEdges; + for (x = 0; x < wEdges; x++) { + for (y = 0; y < lEdges; y++) { + if (x < widthSegments && y < lengthSegments) { + createFace(indices, vertices, baseIndex + (x + 1)*lEdges + y + 1, baseIndex + (x + 1)*lEdges + y, baseIndex + x*lEdges + y, baseIndex + x*lEdges + y + 1, 0, 0, -1, -1, 0, 0, -1, reverse); + } + } + } + } + + // Set bounds + geometry = new Geometry(); + geometry._indices = indices; + var attributes:Array = new Array; + attributes[0] = VertexAttributes.POSITION; + attributes[1] = VertexAttributes.POSITION; + attributes[2] = VertexAttributes.POSITION; + attributes[3] = VertexAttributes.TEXCOORDS[0]; + attributes[4] = VertexAttributes.TEXCOORDS[0]; + attributes[5] = VertexAttributes.NORMAL; + attributes[6] = VertexAttributes.NORMAL; + attributes[7] = VertexAttributes.NORMAL; + attributes[8] = VertexAttributes.TANGENT4; + attributes[9] = VertexAttributes.TANGENT4; + attributes[10] = VertexAttributes.TANGENT4; + attributes[11] = VertexAttributes.TANGENT4; + + geometry.addVertexStream(attributes); + geometry._vertexStreams[0].data = vertices; + geometry._numVertices = vertices.length/48; + if (!twoSided) { + addSurface(top, 0, indices.length/3); + } else { + addSurface(top, 0, indices.length/6); + addSurface(bottom, indices.length/2 , indices.length/6); + } + + boundBox = new BoundBox(); + boundBox.minX = -halfWidth; + boundBox.minY = -halfLength; + boundBox.minZ = 0; + boundBox.maxX = halfWidth; + boundBox.maxY = halfLength; + boundBox.maxZ = 0; + } + + private function createFace(indices:Vector., vertices:ByteArray, a:int, b:int, c:int, d:int, nx:Number, ny:Number, nz:Number, tx:Number, ty:Number, tz:Number, tw:Number, reverse:Boolean):void { + var temp:int; + if (reverse) { + nx = -nx; + ny = -ny; + nz = -nz; + tw = -tw; + temp = a; + a = d; + d = temp; + temp = b; + b = c; + c = temp; + } + indices.push(a); + indices.push(b); + indices.push(c); + indices.push(a); + indices.push(c); + indices.push(d); + vertices.position = a*48 + 20; + vertices.writeFloat(nx); + vertices.writeFloat(ny); + vertices.writeFloat(nz); + vertices.writeFloat(tx); + vertices.writeFloat(ty); + vertices.writeFloat(tz); + vertices.writeFloat(tw); + vertices.position = b*48 + 20; + vertices.writeFloat(nx); + vertices.writeFloat(ny); + vertices.writeFloat(nz); + vertices.writeFloat(tx); + vertices.writeFloat(ty); + vertices.writeFloat(tz); + vertices.writeFloat(tw); + vertices.position = c*48 + 20; + vertices.writeFloat(nx); + vertices.writeFloat(ny); + vertices.writeFloat(nz); + vertices.writeFloat(tx); + vertices.writeFloat(ty); + vertices.writeFloat(tz); + vertices.writeFloat(tw); + vertices.position = d*48 + 20; + vertices.writeFloat(nx); + vertices.writeFloat(ny); + vertices.writeFloat(nz); + vertices.writeFloat(tx); + vertices.writeFloat(ty); + vertices.writeFloat(tz); + vertices.writeFloat(tw); + } + + /** + * @inheritDoc + */ + override public function clone():Object3D { + var res:Plane = new Plane(0, 0, 0, 0); + res.clonePropertiesFrom(this); + return res; + } + + } +} diff --git a/src/alternativa/engine3d/resources/ATFTextureResource.as b/src/alternativa/engine3d/resources/ATFTextureResource.as new file mode 100644 index 0000000..3130478 --- /dev/null +++ b/src/alternativa/engine3d/resources/ATFTextureResource.as @@ -0,0 +1,111 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.resources { + + import alternativa.engine3d.alternativa3d; + + import flash.display3D.Context3D; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.textures.CubeTexture; + import flash.display3D.textures.Texture; + import flash.events.Event; + import flash.utils.ByteArray; + + use namespace alternativa3d; + + /** + * Allows to upload in textures of ATF format to GPU. + * + * @see alternativa.engine3d.resources.BitmapTextureResource + * @see alternativa.engine3d.resources.ExternalTextureResource + * @see alternativa.engine3d.resources.TextureResource + */ + public class ATFTextureResource extends TextureResource { + /** + * ByteArray, that contains texture of ATF format. + */ + public var data:ByteArray; + + private var uploadCallback:Function = null; + + /** + * Create an instance of CompressedTextureResource. + * @param data ByteArray, that contains ATF texture. + */ + public function ATFTextureResource(data:ByteArray) { + this.data = data; + } + + /** + * @inheritDoc + */ + override public function upload(context3D:Context3D):void { + uploadInternal(context3D); + } + + public function uploadAsync(context3D:Context3D, callback:Function):void { + uploadInternal(context3D, true, callback); + } + + private function uploadInternal(context3D:Context3D, async:Boolean = false, callback:Function = null):void { + if (_texture != null) _texture.dispose(); + + if (data != null) { + data.position = 6; + var type:uint = data.readByte(); + var format:String; + switch (type & 0x7F) { + case 0: + format = Context3DTextureFormat.BGRA; + break; + case 1: + format = Context3DTextureFormat.BGRA; + break; + case 2: + format = Context3DTextureFormat.COMPRESSED; + break; + } + + if ((type & ~0x7F) == 0) { + _texture = context3D.createTexture(1 << data.readByte(), 1 << data.readByte(), format, false); + if (async) { + uploadCallback = callback; + _texture.addEventListener("textureReady", onTextureReady); + Texture(_texture).uploadCompressedTextureFromByteArray(data, 0, true); + } else { + Texture(_texture).uploadCompressedTextureFromByteArray(data, 0, false); + } + + } else { + _texture = context3D.createCubeTexture(1 << data.readByte(), format, false); + if (async) { + uploadCallback = callback; + _texture.addEventListener("textureReady", onTextureReady); + CubeTexture(_texture).uploadCompressedTextureFromByteArray(data, 0, true); + } else { + CubeTexture(_texture).uploadCompressedTextureFromByteArray(data, 0, false); + } + } + } else { + _texture = null; + throw new Error("Cannot upload without data"); + } + } + + private function onTextureReady(e:Event):void { + if (uploadCallback != null) { + uploadCallback(this); + uploadCallback = null; + } + } + + } +} diff --git a/src/alternativa/engine3d/resources/BitmapCubeTextureResource.as b/src/alternativa/engine3d/resources/BitmapCubeTextureResource.as new file mode 100644 index 0000000..336410a --- /dev/null +++ b/src/alternativa/engine3d/resources/BitmapCubeTextureResource.as @@ -0,0 +1,255 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.resources { + + import alternativa.engine3d.alternativa3d; + + import flash.display.BitmapData; + import flash.display3D.Context3D; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.textures.CubeTexture; + import flash.filters.ConvolutionFilter; + import flash.geom.Matrix; + import flash.geom.Point; + import flash.geom.Rectangle; + + use namespace alternativa3d; + + /** + * Resource of cube texture. + * + * Allows user to upload cube texture, that consists of six BitmapData images to GPU. + * Size of texture must be power of two (e.g., 256х256, 128*512, 256* 32). + * @see alternativa.engine3d.resources.TextureResource + * @see alternativa.engine3d.resources.ATFTextureResource + * @see alternativa.engine3d.resources.ExternalTextureResource + */ + public class BitmapCubeTextureResource extends TextureResource { + + static private const filter:ConvolutionFilter = new ConvolutionFilter(2, 2, [1, 1, 1, 1], 4, 0, false, true); + + static private var temporaryBitmapData:BitmapData; + static private const rect:Rectangle = new Rectangle(); + static private const point:Point = new Point(); + static private const matrix:Matrix = new Matrix(0.5, 0, 0, 0.5); + + /** + * BitmapData, that will be used as left face. + */ + public var left:BitmapData; + /** + * BitmapData, that will be used as right face. + */ + public var right:BitmapData; + /** + * BitmapData, that will be used as top face. + */ + public var top:BitmapData; + /** + * BitmapData, that will be used as bottom face. + */ + public var bottom:BitmapData; + /** + * BitmapData, that will be used as front face. + */ + public var front:BitmapData; + /** + * BitmapData, that will be used as back face. + */ + public var back:BitmapData; + /** + * Property, that define the choice of type of coordinate system: left-side or right-side. + * If axis Y is directed to up, and axis X - to front, then if you use right-side coordinate + * system, axis Z is directed to right. But if you use right-side coordinate system, then + * axis Z is directed to left. + */ + public var leftHanded:Boolean; + + /** + * Creates a new instance of BitmapCubeTextureResource. + * @param left BitmapData, that will be used as left face. + * @param right BitmapData, that will be used as right face. + * @param bottom BitmapData, that will be used as bottom face. + * @param top BitmapData, that will be used as top face. + * @param back BitmapData, that will be used as back face. + * @param front BitmapData, that will be used as front face. + * @param leftHanded Property, that define the choice of type of coordinate system: left-side or right-side. + */ + public function BitmapCubeTextureResource(left:BitmapData, right:BitmapData, back:BitmapData, front:BitmapData, bottom:BitmapData, top:BitmapData, leftHanded:Boolean = false) { + this.left = left; + this.right = right; + this.bottom = bottom; + this.top = top; + this.back = back; + this.front = front; + this.leftHanded = leftHanded; + } + + /** + * @inheritDoc + */ + override public function upload(context3D:Context3D):void { + if (_texture != null) _texture.dispose(); + _texture = context3D.createCubeTexture(left.width, Context3DTextureFormat.BGRA, false); + var cubeTexture:CubeTexture = CubeTexture(_texture); + filter.preserveAlpha = !left.transparent; + var bmp:BitmapData = (temporaryBitmapData != null) ? temporaryBitmapData : new BitmapData(left.width, left.height, left.transparent); + + var level:int = 0; + + + + var current:BitmapData; + + if (leftHanded) { + current = left; + } else { + current = new BitmapData(left.width, left.height, left.transparent); + current.draw(left, new Matrix(0, -1, -1, 0, left.width, left.height)); + } + cubeTexture.uploadFromBitmapData(current, 1, level++); + + rect.width = left.width; + rect.height = left.height; + while (rect.width%2 == 0 || rect.height%2 == 0) { + bmp.applyFilter(current, rect, point, filter); + rect.width >>= 1; + rect.height >>= 1; + if (rect.width == 0) rect.width = 1; + if (rect.height == 0) rect.height = 1; + if (current != left) current.dispose(); + current = new BitmapData(rect.width, rect.height, left.transparent, 0); + current.draw(bmp, matrix, null, null, null, false); + cubeTexture.uploadFromBitmapData(current, 1, level++); + } + + level = 0; + if (leftHanded) { + current = right; + } else { + current = new BitmapData(right.width, right.height, right.transparent); + current.draw(right, new Matrix(0, 1, 1, 0)); + } + + cubeTexture.uploadFromBitmapData(current, 0, level++); + rect.width = right.width; + rect.height = right.height; + while (rect.width%2 == 0 || rect.height%2 == 0) { + bmp.applyFilter(current, rect, point, filter); + rect.width >>= 1; + rect.height >>= 1; + if (rect.width == 0) rect.width = 1; + if (rect.height == 0) rect.height = 1; + if (current != right) current.dispose(); + current = new BitmapData(rect.width, rect.height, right.transparent, 0); + current.draw(bmp, matrix, null, null, null, false); + cubeTexture.uploadFromBitmapData(current, 0, level++); + } + + level = 0; + + + if (leftHanded) { + current = back; + } else { + current = new BitmapData(back.width, back.height, back.transparent); + current.draw(back, new Matrix(-1, 0, 0, 1, back.width, 0)); + } + + cubeTexture.uploadFromBitmapData(current, 3, level++); + + rect.width = back.width; + rect.height = back.height; + while (rect.width%2 == 0 || rect.height%2 == 0) { + bmp.applyFilter(current, rect, point, filter); + rect.width >>= 1; + rect.height >>= 1; + if (rect.width == 0) rect.width = 1; + if (rect.height == 0) rect.height = 1; + if (current != back) current.dispose(); + current = new BitmapData(rect.width, rect.height, back.transparent, 0); + current.draw(bmp, matrix, null, null, null, false); + cubeTexture.uploadFromBitmapData(current, 3, level++); + } + + level = 0; + if (leftHanded) { + current = front; + } else { + current = new BitmapData(front.width, front.height, front.transparent); + current.draw(front, new Matrix(1, 0, 0, -1, 0, front.height)); + } + cubeTexture.uploadFromBitmapData(current, 2, level++); + + rect.width = front.width; + rect.height = front.height; + while (rect.width%2 == 0 || rect.height%2 == 0) { + bmp.applyFilter(current, rect, point, filter); + rect.width >>= 1; + rect.height >>= 1; + if (rect.width == 0) rect.width = 1; + if (rect.height == 0) rect.height = 1; + if (current != front) current.dispose(); + current = new BitmapData(rect.width, rect.height, front.transparent, 0); + current.draw(bmp, matrix, null, null, null, false); + cubeTexture.uploadFromBitmapData(current, 2, level++); + } + + level = 0; + if (leftHanded) { + current = bottom; + } else { + current = new BitmapData(bottom.width, bottom.height, bottom.transparent); + current.draw(bottom, new Matrix(-1, 0, 0, 1, bottom.width, 0)); + } + cubeTexture.uploadFromBitmapData(current, 5, level++); + + rect.width = bottom.width; + rect.height = bottom.height; + while (rect.width%2 == 0 || rect.height%2 == 0) { + bmp.applyFilter(current, rect, point, filter); + rect.width >>= 1; + rect.height >>= 1; + if (rect.width == 0) rect.width = 1; + if (rect.height == 0) rect.height = 1; + if (current != bottom) current.dispose(); + current = new BitmapData(rect.width, rect.height, bottom.transparent, 0); + current.draw(bmp, matrix, null, null, null, false); + cubeTexture.uploadFromBitmapData(current, 5, level++); + } + + level = 0; + if (leftHanded) { + current = top; + } else { + current = new BitmapData(top.width, top.height, top.transparent); + current.draw(top, new Matrix(1, 0, 0, -1, 0, top.height)); + } + cubeTexture.uploadFromBitmapData(current, 4, level++); + + rect.width = top.width; + rect.height = top.height; + while (rect.width%2 == 0 || rect.height%2 == 0) { + bmp.applyFilter(current, rect, point, filter); + rect.width >>= 1; + rect.height >>= 1; + if (rect.width == 0) rect.width = 1; + if (rect.height == 0) rect.height = 1; + if (current != top) current.dispose(); + current = new BitmapData(rect.width, rect.height, top.transparent, 0); + current.draw(bmp, matrix, null, null, null, false); + cubeTexture.uploadFromBitmapData(current, 4, level++); + } + if (temporaryBitmapData == null) bmp.dispose(); + } + + } +} diff --git a/src/alternativa/engine3d/resources/BitmapTextureResource.as b/src/alternativa/engine3d/resources/BitmapTextureResource.as new file mode 100644 index 0000000..31470fb --- /dev/null +++ b/src/alternativa/engine3d/resources/BitmapTextureResource.as @@ -0,0 +1,133 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.resources { + + import alternativa.engine3d.alternativa3d; + + import flash.display.BitmapData; + import flash.display3D.Context3D; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.textures.Texture; + import flash.filters.ConvolutionFilter; + import flash.geom.Matrix; + import flash.geom.Point; + import flash.geom.Rectangle; + + use namespace alternativa3d; + + /** + * Texture resource, that allows user to upload textures from BitmapData to GPU. + * Size of texture must be power of two (e.g., 256х256, 128*512, 256* 32). + * @see alternativa.engine3d.resources.TextureResource + * @see alternativa.engine3d.resources.ATFTextureResource + * @see alternativa.engine3d.resources.ExternalTextureResource + */ + public class BitmapTextureResource extends TextureResource { + + static private const rect:Rectangle = new Rectangle(); + static private const filter:ConvolutionFilter = new ConvolutionFilter(2, 2, [1, 1, 1, 1], 4, 0, false, true); + static private const matrix:Matrix = new Matrix(0.5, 0, 0, 0.5); + static private const point:Point = new Point(); + /** + * BitmapData + */ + public var data:BitmapData; + + /** + * Uploads textures from BitmapData to GPU. + */ + public function BitmapTextureResource(data:BitmapData) { + this.data = data; + } + + /** + * @inheritDoc + */ + override public function upload(context3D:Context3D):void { + if (_texture != null) _texture.dispose(); + if (data != null) { + _texture = context3D.createTexture(data.width, data.height, Context3DTextureFormat.BGRA, false); + filter.preserveAlpha = !data.transparent; + Texture(_texture).uploadFromBitmapData(data, 0); + var level:int = 1; + var bmp:BitmapData = new BitmapData(data.width, data.height, data.transparent); + var current:BitmapData = data; + rect.width = data.width; + rect.height = data.height; + while (rect.width%2 == 0 || rect.height%2 == 0) { + bmp.applyFilter(current, rect, point, filter); + rect.width >>= 1; + rect.height >>= 1; + if (rect.width == 0) rect.width = 1; + if (rect.height == 0) rect.height = 1; + if (current != data) current.dispose(); + current = new BitmapData(rect.width, rect.height, data.transparent, 0); + current.draw(bmp, matrix, null, null, null, false); + Texture(_texture).uploadFromBitmapData(current, level++); + } + if (current != data) current.dispose(); + bmp.dispose(); + } else { + _texture = null; + throw new Error("Cannot upload without data"); + } + } + + /** + * @private + */ + alternativa3d function createMips(texture:Texture, bitmapData:BitmapData):void { + rect.width = bitmapData.width; + rect.height = bitmapData.height; + var level:int = 1; + var bmp:BitmapData = new BitmapData(rect.width, rect.height, bitmapData.transparent); + var current:BitmapData = bitmapData; + while (rect.width%2 == 0 || rect.height%2 == 0) { + bmp.applyFilter(current, rect, point, filter); + rect.width >>= 1; + rect.height >>= 1; + if (rect.width == 0) rect.width = 1; + if (rect.height == 0) rect.height = 1; + if (current != bitmapData) current.dispose(); + current = new BitmapData(rect.width, rect.height, bitmapData.transparent, 0); + current.draw(bmp, matrix, null, null, null, false); + texture.uploadFromBitmapData(current, level++); + } + if (current != bitmapData) current.dispose(); + bmp.dispose(); + } + + /** + * @private + */ + static alternativa3d function createMips(texture:Texture, bitmapData:BitmapData):void { + rect.width = bitmapData.width; + rect.height = bitmapData.height; + var level:int = 1; + var bmp:BitmapData = new BitmapData(rect.width, rect.height, bitmapData.transparent); + var current:BitmapData = bitmapData; + while (rect.width%2 == 0 || rect.height%2 == 0) { + bmp.applyFilter(current, rect, point, filter); + rect.width >>= 1; + rect.height >>= 1; + if (rect.width == 0) rect.width = 1; + if (rect.height == 0) rect.height = 1; + if (current != bitmapData) current.dispose(); + current = new BitmapData(rect.width, rect.height, bitmapData.transparent, 0); + current.draw(bmp, matrix, null, null, null, false); + texture.uploadFromBitmapData(current, level++); + } + if (current != bitmapData) current.dispose(); + bmp.dispose(); + } + + } +} diff --git a/src/alternativa/engine3d/resources/ExternalTextureResource.as b/src/alternativa/engine3d/resources/ExternalTextureResource.as new file mode 100644 index 0000000..0636f92 --- /dev/null +++ b/src/alternativa/engine3d/resources/ExternalTextureResource.as @@ -0,0 +1,61 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.resources { + + import alternativa.engine3d.alternativa3d; + + import flash.display3D.Context3D; + import flash.display3D.textures.TextureBase; + + use namespace alternativa3d; + + /** + * a TextureResource, that uses data from external source (file). Link for this source is specified on instance creation. + * Used with TexturesLoader, which gets ExternalTextureResource for uploading needed files. + * Size of texture must be power of two (e.g., 256х256, 128*512, 256* 32). + * @see alternativa.engine3d.loaders.TexturesLoader#loadResource() + * @see alternativa.engine3d.loaders.TexturesLoader#loadResources() + */ + public class ExternalTextureResource extends TextureResource { + /** + * URL-path to texture. + */ + public var url:String; + + /** + * @param url Adress of texture. + */ + public function ExternalTextureResource(url:String) { + this.url = url; + } + + /** + * @inheritDoc + */ + override public function upload(context3D:Context3D):void { + } + + /** + * Resource data, that are get from external resource. + */ + public function get data():TextureBase { + return _texture; + } + + /** + * @private + */ + public function set data(value:TextureBase):void { + _texture = value; + } + + } +} diff --git a/src/alternativa/engine3d/resources/Geometry.as b/src/alternativa/engine3d/resources/Geometry.as new file mode 100644 index 0000000..45fc80f --- /dev/null +++ b/src/alternativa/engine3d/resources/Geometry.as @@ -0,0 +1,1016 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.resources { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.RayIntersectionData; + import alternativa.engine3d.core.Resource; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.core.VertexStream; + + import flash.display3D.Context3D; + import flash.display3D.IndexBuffer3D; + import flash.display3D.VertexBuffer3D; + import flash.geom.Point; + import flash.geom.Vector3D; + import flash.utils.ByteArray; + import flash.utils.Endian; + + use namespace alternativa3d; + /** + * Resource, that stores data about geometry of object. All data are stored for each vertex. + * So, you can set any set of parameters. And this set will be defined for each vertex of geometry. + * It will be useful to divide parameters by some vertexBuffers in order to update these data at + * memory of GPU, independently of each other (vertexBuffer can be updated at once only). + * For this, you can store groups of parameters in different streams. Based on them vertexBuffers will be formed on uploading to GPU. + * When new stream is formed, are specified the parameters, that will be stored in it. + * @example This code creates stream on properties: x,y,z,u,v and forms a triangle by three vertices. + * + * var attributes:Array = new Array(); + * attributes[0] = VertexAttributes.POSITION; + * attributes[1] = VertexAttributes.POSITION; + * attributes[2] = VertexAttributes.POSITION; + * attributes[3] = VertexAttributes.TEXCOORDS[0]; + * attributes[4] = VertexAttributes.TEXCOORDS[0]; + * var geometry = new Geometry(); + * geometry.addVertexStream(attributes); + * geometry.numVertices = 3; + * geometry.setAttributeValues(VertexAttributes.POSITION, new [x1,y1,z1,x2,y2,z2,x3,y3,z3]); + * geometry.setAttributeValues(VertexAttributes.TEXCOORDS[0], new [u1,v1,u2,v2,u3,v3]); + * geometry.indices = Vector.([0,1,2]); + * + * To get access to data, you can use method getAttributeValues by parameter name, e.g.: + * geometry.getAttributeValues(VertexAttributes.POSITION) + * returns vector from coordinates: [x1,y1,z1,x2,y2,z2,x3,y3,z3]. + */ + public class Geometry extends Resource { + + /** + * @private + */ + alternativa3d var _vertexStreams:Vector. = new Vector.(); + + /** + * @private + */ + alternativa3d var _indexBuffer:IndexBuffer3D; + + /** + * @private + */ + alternativa3d var _numVertices:int; + + /** + * @private + */ + alternativa3d var _indices:Vector. = new Vector.(); + + /** + * @private + */ + alternativa3d var _attributesStreams:Vector. = new Vector.(); + + /** + * @private + */ + alternativa3d var _attributesOffsets:Vector. = new Vector.(); + + private var _attributesStrides:Vector. = new Vector.(); + + /** + * Creates a new instance. + * @param numVertices Number of vertices. + */ + public function Geometry(numVertices:int = 0) { + this._numVertices = numVertices; + } + + /** + * Number of triangles, that are contained in geometry. + */ + public function get numTriangles():int { + return _indices.length/3; + } + + /** + * Indexes of vertices for specifying of triangles of surface. + * Example of specifying of surface, that consists of two triangles: Vector.([vertex_id_1,vertex_id_2,vertex_id_3,vertex_id_4,vertex_id_5,vertex_id_6]);. + */ + public function get indices():Vector. { + return _indices.slice(); + } + + /** + * @private + */ + public function set indices(value:Vector.):void { + if (value == null) { + _indices.length = 0; + } else { + _indices = value.slice() + } + } + + /** + * Number of vertices of geometry. + */ + public function get numVertices():int { + return _numVertices; + } + + /** + * @private + */ + public function set numVertices(value:int):void { + if (_numVertices != value) { + // Change buffers. + for each (var vBuffer:VertexStream in _vertexStreams) { + var numMappings:int = vBuffer.attributes.length; + vBuffer.data.length = 4*numMappings*value; + } + _numVertices = value; + } + } + + /** + * Calculation of vertex normals. + */ + public function calculateNormals():void { + if (!hasAttribute(VertexAttributes.POSITION)) throw new Error("Vertices positions is required to calculate normals"); + var normals:Array = new Array(); + var positionsStream:VertexStream = _attributesStreams[VertexAttributes.POSITION]; + var positionsData:ByteArray = positionsStream.data; + var positionsOffset:int = _attributesOffsets[VertexAttributes.POSITION]*4; + var stride:int = positionsStream.attributes.length*4; + var numIndices:int = _indices.length; + var normal:Vector3D; + var i:int; + // Normals calculations + for (i = 0; i < numIndices; i += 3) { + var vertIndexA:int = _indices[i]; + var vertIndexB:int = _indices[i + 1]; + var vertIndexC:int = _indices[i + 2]; + // v1 + positionsData.position = vertIndexA*stride + positionsOffset; + var ax:Number = positionsData.readFloat(); + var ay:Number = positionsData.readFloat(); + var az:Number = positionsData.readFloat(); + + // v2 + positionsData.position = vertIndexB*stride + positionsOffset; + var bx:Number = positionsData.readFloat(); + var by:Number = positionsData.readFloat(); + var bz:Number = positionsData.readFloat(); + + // v3 + positionsData.position = vertIndexC*stride + positionsOffset; + var cx:Number = positionsData.readFloat(); + var cy:Number = positionsData.readFloat(); + var cz:Number = positionsData.readFloat(); + + // v2-v1 + var abx:Number = bx - ax; + var aby:Number = by - ay; + var abz:Number = bz - az; + + // v3-v1 + var acx:Number = cx - ax; + var acy:Number = cy - ay; + var acz:Number = cz - az; + + var normalX:Number = acz*aby - acy*abz; + var normalY:Number = acx*abz - acz*abx; + var normalZ:Number = acy*abx - acx*aby; + + var normalLen:Number = Math.sqrt(normalX*normalX + normalY*normalY + normalZ*normalZ); + + if (normalLen > 0) { + normalX /= normalLen; + normalY /= normalLen; + normalZ /= normalLen; + } else { + trace("degenerated triangle", i/3); + } + + // v1 normal + normal = normals[vertIndexA]; + + if (normal == null) { + normals[vertIndexA] = new Vector3D(normalX, normalY, normalZ); + } else { + normal.x += normalX; + normal.y += normalY; + normal.z += normalZ; + } + + // v2 normal + normal = normals[vertIndexB]; + + if (normal == null) { + normals[vertIndexB] = new Vector3D(normalX, normalY, normalZ); + } else { + normal.x += normalX; + normal.y += normalY; + normal.z += normalZ; + } + + // v3 normal + normal = normals[vertIndexC]; + + if (normal == null) { + normals[vertIndexC] = new Vector3D(normalX, normalY, normalZ); + } else { + normal.x += normalX; + normal.y += normalY; + normal.z += normalZ; + } + } + + if (hasAttribute(VertexAttributes.NORMAL)) { + + var normalsOffset:int = _attributesOffsets[VertexAttributes.NORMAL]*4; + var normalsStream:VertexStream = _attributesStreams[VertexAttributes.NORMAL]; + var normalsBuffer:ByteArray = normalsStream.data; + var normalsBufferStride:uint = normalsStream.attributes.length*4; + for (i = 0; i < _numVertices; i++) { + normal = normals[i]; + normal.normalize(); + normalsBuffer.position = i*normalsBufferStride + normalsOffset; + normalsBuffer.writeFloat(normal.x); + normalsBuffer.writeFloat(normal.y); + normalsBuffer.writeFloat(normal.z); + } + } else { + // Write normals to ByteArray + var resultByteArray:ByteArray = new ByteArray(); + resultByteArray.endian = Endian.LITTLE_ENDIAN; + for (i = 0; i < _numVertices; i++) { + normal = normals[i]; + normal.normalize(); + resultByteArray.writeBytes(positionsData, i*stride, stride); + resultByteArray.writeFloat(normal.x); + resultByteArray.writeFloat(normal.y); + resultByteArray.writeFloat(normal.z); + } + positionsStream.attributes.push(VertexAttributes.NORMAL); + positionsStream.attributes.push(VertexAttributes.NORMAL); + positionsStream.attributes.push(VertexAttributes.NORMAL); + + positionsStream.data = resultByteArray; + positionsData.clear(); + + _attributesOffsets[VertexAttributes.NORMAL] = stride/4; + _attributesStreams[VertexAttributes.NORMAL] = positionsStream; + _attributesStrides[VertexAttributes.NORMAL] = 3; + } + + } + + /** + * Calculation of tangents and bi-normals. Normals of geometry must be calculated. + */ + public function calculateTangents(uvChannel:int):void { + if (!hasAttribute(VertexAttributes.POSITION)) throw new Error("Vertices positions is required to calculate normals"); + if (!hasAttribute(VertexAttributes.NORMAL)) throw new Error("Vertices normals is required to calculate tangents, call calculateNormals first"); + if (!hasAttribute(VertexAttributes.TEXCOORDS[uvChannel])) throw new Error("Specified uv channel does not exist in geometry"); + + var tangents:Array = new Array(); + + var positionsStream:VertexStream = _attributesStreams[VertexAttributes.POSITION]; + var positionsData:ByteArray = positionsStream.data; + var positionsOffset:int = _attributesOffsets[VertexAttributes.POSITION]*4; + var positionsStride:int = positionsStream.attributes.length*4; + + var normalsStream:VertexStream = _attributesStreams[VertexAttributes.NORMAL]; + var normalsData:ByteArray = normalsStream.data; + var normalsOffset:int = _attributesOffsets[VertexAttributes.NORMAL]*4; + var normalsStride:int = normalsStream.attributes.length*4; + + var uvsStream:VertexStream = _attributesStreams[VertexAttributes.TEXCOORDS[uvChannel]]; + var uvsData:ByteArray = uvsStream.data; + var uvsOffset:int = _attributesOffsets[VertexAttributes.TEXCOORDS[uvChannel]]*4; + var uvsStride:int = uvsStream.attributes.length*4; + + var numIndices:int = _indices.length; + var normal:Vector3D; + var tangent:Vector3D; + var i:int; + + for (i = 0; i < numIndices; i += 3) { + var vertIndexA:int = _indices[i]; + var vertIndexB:int = _indices[i + 1]; + var vertIndexC:int = _indices[i + 2]; + + // a.xyz + positionsData.position = vertIndexA*positionsStride + positionsOffset; + var ax:Number = positionsData.readFloat(); + var ay:Number = positionsData.readFloat(); + var az:Number = positionsData.readFloat(); + + // b.xyz + positionsData.position = vertIndexB*positionsStride + positionsOffset; + var bx:Number = positionsData.readFloat(); + var by:Number = positionsData.readFloat(); + var bz:Number = positionsData.readFloat(); + + // c.xyz + positionsData.position = vertIndexC*positionsStride + positionsOffset; + var cx:Number = positionsData.readFloat(); + var cy:Number = positionsData.readFloat(); + var cz:Number = positionsData.readFloat(); + + // a.uv + uvsData.position = vertIndexA*uvsStride + uvsOffset; + var au:Number = uvsData.readFloat(); + var av:Number = uvsData.readFloat(); + + // b.uv + uvsData.position = vertIndexB*uvsStride + uvsOffset; + var bu:Number = uvsData.readFloat(); + var bv:Number = uvsData.readFloat(); + + // c.uv + uvsData.position = vertIndexC*uvsStride + uvsOffset; + var cu:Number = uvsData.readFloat(); + var cv:Number = uvsData.readFloat(); + + // a.nrm + normalsData.position = vertIndexA*normalsStride + normalsOffset; + var anx:Number = normalsData.readFloat(); + var any:Number = normalsData.readFloat(); + var anz:Number = normalsData.readFloat(); + + // b.nrm + normalsData.position = vertIndexB*normalsStride + normalsOffset; + var bnx:Number = normalsData.readFloat(); + var bny:Number = normalsData.readFloat(); + var bnz:Number = normalsData.readFloat(); + + // c.nrm + normalsData.position = vertIndexC*normalsStride + normalsOffset; + var cnx:Number = normalsData.readFloat(); + var cny:Number = normalsData.readFloat(); + var cnz:Number = normalsData.readFloat(); + + // v2-v1 + var abx:Number = bx - ax; + var aby:Number = by - ay; + var abz:Number = bz - az; + + // v3-v1 + var acx:Number = cx - ax; + var acy:Number = cy - ay; + var acz:Number = cz - az; + + var abu:Number = bu - au; + var abv:Number = bv - av; + + var acu:Number = cu - au; + var acv:Number = cv - av; + + var r:Number = 1/(abu*acv - acu*abv); + + var tangentX:Number = r*(acv*abx - acx*abv); + var tangentY:Number = r*(acv*aby - abv*acy); + var tangentZ:Number = r*(acv*abz - abv*acz); + + tangent = tangents[vertIndexA]; + + if (tangent == null) { + tangents[vertIndexA] = new Vector3D( + tangentX - anx*(anx*tangentX + any*tangentY + anz*tangentZ), + tangentY - any*(anx*tangentX + any*tangentY + anz*tangentZ), + tangentZ - anz*(anx*tangentX + any*tangentY + anz*tangentZ)); + + } else { + tangent.x += tangentX - anx*(anx*tangentX + any*tangentY + anz*tangentZ); + tangent.y += tangentY - any*(anx*tangentX + any*tangentY + anz*tangentZ); + tangent.z += tangentZ - anz*(anx*tangentX + any*tangentY + anz*tangentZ); + } + + tangent = tangents[vertIndexB]; + + if (tangent == null) { + tangents[vertIndexB] = new Vector3D( + tangentX - bnx*(bnx*tangentX + bny*tangentY + bnz*tangentZ), + tangentY - bny*(bnx*tangentX + bny*tangentY + bnz*tangentZ), + tangentZ - bnz*(bnx*tangentX + bny*tangentY + bnz*tangentZ)); + + } else { + tangent.x += tangentX - bnx*(bnx*tangentX + bny*tangentY + bnz*tangentZ); + tangent.y += tangentY - bny*(bnx*tangentX + bny*tangentY + bnz*tangentZ); + tangent.z += tangentZ - bnz*(bnx*tangentX + bny*tangentY + bnz*tangentZ); + } + + tangent = tangents[vertIndexC]; + + if (tangent == null) { + tangents[vertIndexC] = new Vector3D( + tangentX - cnx*(cnx*tangentX + cny*tangentY + cnz*tangentZ), + tangentY - cny*(cnx*tangentX + cny*tangentY + cnz*tangentZ), + tangentZ - cnz*(cnx*tangentX + cny*tangentY + cnz*tangentZ)); + + } else { + tangent.x += tangentX - cnx*(cnx*tangentX + cny*tangentY + cnz*tangentZ); + tangent.y += tangentY - cny*(cnx*tangentX + cny*tangentY + cnz*tangentZ); + tangent.z += tangentZ - cnz*(cnx*tangentX + cny*tangentY + cnz*tangentZ); + } + + } + + if (hasAttribute(VertexAttributes.TANGENT4)) { + + var tangentsOffset:int = _attributesOffsets[VertexAttributes.TANGENT4]*4; + var tangentsStream:VertexStream = _attributesStreams[VertexAttributes.TANGENT4]; + var tangentsBuffer:ByteArray = tangentsStream.data; + var tangentsBufferStride:uint = tangentsStream.attributes.length*4; + for (i = 0; i < _numVertices; i++) { + tangent = tangents[i]; + tangent.normalize(); + tangentsBuffer.position = i*tangentsBufferStride + tangentsOffset; + tangentsBuffer.writeFloat(tangent.x); + tangentsBuffer.writeFloat(tangent.y); + tangentsBuffer.writeFloat(tangent.z); + tangentsBuffer.writeFloat(-1); + } + } else { + // Write normals to ByteArray + var resultByteArray:ByteArray = new ByteArray(); + resultByteArray.endian = Endian.LITTLE_ENDIAN; + for (i = 0; i < _numVertices; i++) { + tangent = tangents[i]; + tangent.normalize(); + resultByteArray.writeBytes(positionsData, i*positionsStride, positionsStride); + resultByteArray.writeFloat(tangent.x); + resultByteArray.writeFloat(tangent.y); + resultByteArray.writeFloat(tangent.z); + resultByteArray.writeFloat(-1); + } + positionsStream.attributes.push(VertexAttributes.TANGENT4); + positionsStream.attributes.push(VertexAttributes.TANGENT4); + positionsStream.attributes.push(VertexAttributes.TANGENT4); + positionsStream.attributes.push(VertexAttributes.TANGENT4); + + positionsStream.data = resultByteArray; + positionsData.clear(); + + _attributesOffsets[VertexAttributes.TANGENT4] = positionsStride/4; + _attributesStreams[VertexAttributes.TANGENT4] = positionsStream; + _attributesStrides[VertexAttributes.TANGENT4] = 4; + } + + } + + /** + * Adds a stream for set of parameters, that can be updated independently of the other sets of parameters. + * @param attributes List of parameters. Types of parameters are get from VertexAttributes. + * @return Index of stream, that has been created. + */ + public function addVertexStream(attributes:Array):int { + var numMappings:int = attributes.length; + if (numMappings < 1) { + throw new Error("Must be at least one attribute ​​to create the buffer."); + } + var vBuffer:VertexStream = new VertexStream(); + var newBufferIndex:int = _vertexStreams.length; + var attribute:uint = attributes[0]; + var stride:int = 1; + for (var i:int = 1; i <= numMappings; i++) { + var next:uint = (i < numMappings) ? attributes[i] : 0; + if (next != attribute) { + // Last item will enter here forcibly. + if (attribute != 0) { + if (attribute < _attributesStreams.length && _attributesStreams[attribute] != null) { + throw new Error("Attribute " + attribute + " already used in this geometry."); + } + var numStandartFloats:int = VertexAttributes.getAttributeStride(attribute); + if (numStandartFloats != 0 && numStandartFloats != stride) { + throw new Error("Standard attributes must be predefined size."); + } + if (_attributesStreams.length < attribute) { + _attributesStreams.length = attribute + 1; + _attributesOffsets.length = attribute + 1; + _attributesStrides.length = attribute + 1; + } + var startIndex:int = i - stride; + _attributesStreams[attribute] = vBuffer; + _attributesOffsets[attribute] = startIndex; + _attributesStrides[attribute] = stride; + } + stride = 1; + } else { + stride++; + } + attribute = next; + } + vBuffer.attributes = attributes.slice(); + // vBuffer.data = new Vector.(numMappings*_numVertices); + vBuffer.data = new ByteArray(); + vBuffer.data.endian = Endian.LITTLE_ENDIAN; + vBuffer.data.length = 4*numMappings*_numVertices; + _vertexStreams[newBufferIndex] = vBuffer; + return newBufferIndex; + } + + /** + * Number of vertex-streams. + */ + public function get numVertexStreams():int { + return _vertexStreams.length; + } + + /** + * returns mapping of stream by index. + * @param index index of stream. + * @return mapping. + */ + public function getVertexStreamAttributes(index:int):Array { + return _vertexStreams[index].attributes.slice(); + } + + /** + * Check the existence of attribute in all streams. + * @param attribute Attribute, that is checked. + * @return + */ + public function hasAttribute(attribute:uint):Boolean { + return attribute < _attributesStreams.length && _attributesStreams[attribute] != null; + } + + /** + * Returns index of stream, that contains needed attribute. + * + * @param attribute + * + * @return -1 if attribute is not found. + */ + public function findVertexStreamByAttribute(attribute:uint):int { + var vBuffer:VertexStream = (attribute < _attributesStreams.length) ? _attributesStreams[attribute] : null; + if (vBuffer != null) { + for (var i:int = 0; i < _vertexStreams.length; i++) { + if (_vertexStreams[i] == vBuffer) { + return i; + } + } + } + return -1; + } + + /** + * Offset of attribute at stream, with which this attribute is stored. You can find index of stream using findVertexStreamByAttribute. + * + * @param attribute Type of attribute. List of types of attributes placed at VertexAttributes. + * @return Offset. + * + * @see #findVertexStreamByAttribute + * @see VertexAttributes + */ + public function getAttributeOffset(attribute:uint):int { + var vBuffer:VertexStream = (attribute < _attributesStreams.length) ? _attributesStreams[attribute] : null; + if (vBuffer == null) { + throw new Error("Attribute not found."); + } + return _attributesOffsets[attribute]; + } + + /** + * Sets value for attribute. + * If buffer has not been initialized, then it initialized with zeros automatically. + * + * @param attribute + * @param values + */ + public function setAttributeValues(attribute:uint, values:Vector.):void { + var vBuffer:VertexStream = (attribute < _attributesStreams.length) ? _attributesStreams[attribute] : null; + if (vBuffer == null) { + throw new Error("Attribute not found."); + } + var stride:int = _attributesStrides[attribute]; + if (values == null || values.length != stride*_numVertices) { + throw new Error("Values count must be the same."); + } + var numMappings:int = vBuffer.attributes.length; + var data:ByteArray = vBuffer.data; + var offset:int = _attributesOffsets[attribute]; + // Copy values + for (var i:int = 0; i < _numVertices; i++) { + var srcIndex:int = stride*i; + data.position = 4*(numMappings*i + offset); + for (var j:int = 0; j < stride; j++) { + data.writeFloat(values[int(srcIndex + j)]); + } + } + } + + public function getAttributeValues(attribute:uint):Vector. { + var vBuffer:VertexStream = (attribute < _attributesStreams.length) ? _attributesStreams[attribute] : null; + if (vBuffer == null) { + throw new Error("Attribute not found."); + } + var data:ByteArray = vBuffer.data; + var stride:int = _attributesStrides[attribute]; + var result:Vector. = new Vector.(stride*_numVertices); + var numMappings:int = vBuffer.attributes.length; + var offset:int = _attributesOffsets[attribute]; + // Copy values + for (var i:int = 0; i < _numVertices; i++) { + data.position = 4*(numMappings*i + offset); + var dstIndex:int = stride*i; + for (var j:int = 0; j < stride; j++) { + result[int(dstIndex + j)] = data.readFloat(); + } + } + return result; + } + + /** + * Check for existence of resource in video memory. + */ + override public function get isUploaded():Boolean { + return _indexBuffer != null; + } + + /** + * @inheritDoc + */ + override public function upload(context3D:Context3D):void { + var vBuffer:VertexStream; + var i:int; + var numBuffers:int = _vertexStreams.length; + if (_indexBuffer != null) { + // Clear old resources + _indexBuffer.dispose(); + _indexBuffer = null; + for (i = 0; i < numBuffers; i++) { + vBuffer = _vertexStreams[i]; + vBuffer.buffer.dispose(); + vBuffer.buffer = null; + } + } + if (_indices.length <= 0 || _numVertices <= 0) { + return; + } + + for (i = 0; i < numBuffers; i++) { + vBuffer = _vertexStreams[i]; + var numMappings:int = vBuffer.attributes.length; + var data:ByteArray = vBuffer.data; + if (data == null) { + throw new Error("Cannot upload without vertex buffer data."); + } + vBuffer.buffer = context3D.createVertexBuffer(_numVertices, numMappings); + vBuffer.buffer.uploadFromByteArray(data, 0, 0, _numVertices); + } + var numIndices:int = _indices.length; + _indexBuffer = context3D.createIndexBuffer(numIndices); + _indexBuffer.uploadFromVector(_indices, 0, numIndices); + } + + /** + * @inheritDoc + */ + override public function dispose():void { + if (_indexBuffer != null) { + _indexBuffer.dispose(); + _indexBuffer = null; + var numBuffers:int = _vertexStreams.length; + for (var i:int = 0; i < numBuffers; i++) { + var vBuffer:VertexStream = _vertexStreams[i]; + vBuffer.buffer.dispose(); + vBuffer.buffer = null; + } + } + } + + /** + * Updates values of index-buffer in video memory. + * @param data List of values. + * @param startOffset Offset. + * @param count Count of updated values. + */ + public function updateIndexBufferInContextFromVector(data:Vector., startOffset:int, count:int):void { + if (_indexBuffer == null) { + throw new Error("Geometry must be uploaded."); + } + _indexBuffer.uploadFromVector(data, startOffset, count); + } + + /** + * Updates values of index-buffer in video memory. + * @param data Data + * @param startOffset Offset + * @param count Number of updated values. + */ + public function updateIndexBufferInContextFromByteArray(data:ByteArray, byteArrayOffset:int, startOffset:int, count:int):void { + if (_indexBuffer == null) { + throw new Error("Geometry must be uploaded."); + } + _indexBuffer.uploadFromByteArray(data, byteArrayOffset, startOffset, count); + } + + /** + * Updates values of vertex-buffer in video memory. + * @param data List of values. + * @param startVertex Offset. + * @param numVertices Number of updated values. + */ + public function updateVertexBufferInContextFromVector(index:int, data:Vector., startVertex:int, numVertices:int):void { + if (_indexBuffer == null) { + throw new Error("Geometry must be uploaded."); + } + _vertexStreams[index].buffer.uploadFromVector(data, startVertex, numVertices); + } + + /** + * Updates values of vertex-buffer in video memory. + * @param data Data + * @param startVertex Offset. + * @param numVertices Number of updated values. + */ + public function updateVertexBufferInContextFromByteArray(index:int, data:ByteArray, byteArrayOffset:int, startVertex:int, numVertices:int):void { + if (_indexBuffer == null) { + throw new Error("Geometry must be uploaded."); + } + _vertexStreams[index].buffer.uploadFromByteArray(data, byteArrayOffset, startVertex, numVertices); + } + + alternativa3d function intersectRay(origin:Vector3D, direction:Vector3D, indexBegin:uint, numTriangles:uint):RayIntersectionData { + var ox:Number = origin.x; + var oy:Number = origin.y; + var oz:Number = origin.z; + var dx:Number = direction.x; + var dy:Number = direction.y; + var dz:Number = direction.z; + + var nax:Number; + var nay:Number; + var naz:Number; + var nau:Number; + var nav:Number; + + var nbx:Number; + var nby:Number; + var nbz:Number; + var nbu:Number; + var nbv:Number; + + var ncx:Number; + var ncy:Number; + var ncz:Number; + var ncu:Number; + var ncv:Number; + + var nrmX:Number; + var nrmY:Number; + var nrmZ:Number; + + var point:Vector3D; + var minTime:Number = 1e+22; + var posAttribute:int = VertexAttributes.POSITION; + var uvAttribute:int = VertexAttributes.TEXCOORDS[0]; + var positionStream:VertexStream; + if (VertexAttributes.POSITION >= _attributesStreams.length || (positionStream = _attributesStreams[posAttribute]) == null) { + throw new Error("Raycast require POSITION attribute"); + } + var positionBuffer:ByteArray = positionStream.data; + // Offset of position attribute. + const positionOffset:uint = _attributesOffsets[posAttribute]*4; + // Length of vertex on bytes. + var stride:uint = positionStream.attributes.length*4; + + var uvStream:VertexStream; + var hasUV:Boolean = uvAttribute < _attributesStreams.length && (uvStream = _attributesStreams[uvAttribute]) != null; + var uvBuffer:ByteArray; + var uvOffset:uint; + var uvStride:uint; + if (hasUV) { + uvBuffer = uvStream.data; + uvOffset = _attributesOffsets[uvAttribute]*4; + uvStride = uvStream.attributes.length*4; + } + + if (numTriangles*3 > indices.length) { + throw new ArgumentError("index is out of bounds"); + } + for (var i:int = indexBegin, count:int = indexBegin + numTriangles*3; i < count; i += 3) { + var indexA:uint = indices[i]; + var indexB:uint = indices[int(i + 1)]; + var indexC:uint = indices[int(i + 2)]; + positionBuffer.position = indexA*stride + positionOffset; + var ax:Number = positionBuffer.readFloat(); + var ay:Number = positionBuffer.readFloat(); + var az:Number = positionBuffer.readFloat(); + var au:Number; + var av:Number; + positionBuffer.position = indexB*stride + positionOffset; + var bx:Number = positionBuffer.readFloat(); + var by:Number = positionBuffer.readFloat(); + var bz:Number = positionBuffer.readFloat(); + var bu:Number; + var bv:Number; + + positionBuffer.position = indexC*stride + positionOffset; + var cx:Number = positionBuffer.readFloat(); + var cy:Number = positionBuffer.readFloat(); + var cz:Number = positionBuffer.readFloat(); + var cu:Number; + var cv:Number; + + if (hasUV) { + uvBuffer.position = indexA*uvStride + uvOffset; + au = uvBuffer.readFloat(); + av = uvBuffer.readFloat(); + + uvBuffer.position = indexB*uvStride + uvOffset; + bu = uvBuffer.readFloat(); + bv = uvBuffer.readFloat(); + + uvBuffer.position = indexC*uvStride + uvOffset; + cu = uvBuffer.readFloat(); + cv = uvBuffer.readFloat(); + } + + var abx:Number = bx - ax; + var aby:Number = by - ay; + var abz:Number = bz - az; + var acx:Number = cx - ax; + var acy:Number = cy - ay; + var acz:Number = cz - az; + var normalX:Number = acz*aby - acy*abz; + var normalY:Number = acx*abz - acz*abx; + var normalZ:Number = acy*abx - acx*aby; + var len:Number = normalX*normalX + normalY*normalY + normalZ*normalZ; + if (len > 0.001) { + len = 1/Math.sqrt(len); + normalX *= len; + normalY *= len; + normalZ *= len; + } + var dot:Number = dx*normalX + dy*normalY + dz*normalZ; + if (dot < 0) { + var offset:Number = ox*normalX + oy*normalY + oz*normalZ - (ax*normalX + ay*normalY + az*normalZ); + if (offset > 0) { + var time:Number = -offset/dot; + if (point == null || time < minTime) { + var rx:Number = ox + dx*time; + var ry:Number = oy + dy*time; + var rz:Number = oz + dz*time; + abx = bx - ax; + aby = by - ay; + abz = bz - az; + acx = rx - ax; + acy = ry - ay; + acz = rz - az; + if ((acz*aby - acy*abz)*normalX + (acx*abz - acz*abx)*normalY + (acy*abx - acx*aby)*normalZ >= 0) { + abx = cx - bx; + aby = cy - by; + abz = cz - bz; + acx = rx - bx; + acy = ry - by; + acz = rz - bz; + if ((acz*aby - acy*abz)*normalX + (acx*abz - acz*abx)*normalY + (acy*abx - acx*aby)*normalZ >= 0) { + abx = ax - cx; + aby = ay - cy; + abz = az - cz; + acx = rx - cx; + acy = ry - cy; + acz = rz - cz; + if ((acz*aby - acy*abz)*normalX + (acx*abz - acz*abx)*normalY + (acy*abx - acx*aby)*normalZ >= 0) { + if (time < minTime) { + minTime = time; + if (point == null) point = new Vector3D(); + point.x = rx; + point.y = ry; + point.z = rz; + nax = ax; + nay = ay; + naz = az; + nau = au; + nav = av; + nrmX = normalX; + nbx = bx; + nby = by; + nbz = bz; + nbu = bu; + nbv = bv; + nrmY = normalY; + ncx = cx; + ncy = cy; + ncz = cz; + ncu = cu; + ncv = cv; + nrmZ = normalZ; + } + } + } + } + } + } + } + } + if (point != null) { + var res:RayIntersectionData = new RayIntersectionData(); + res.point = point; + res.time = minTime; + if (hasUV) { + // Calculation of UV. + abx = nbx - nax; + aby = nby - nay; + abz = nbz - naz; + var abu:Number = nbu - nau; + var abv:Number = nbv - nav; + + acx = ncx - nax; + acy = ncy - nay; + acz = ncz - naz; + var acu:Number = ncu - nau; + var acv:Number = ncv - nav; + + // Calculation of uv-transformation matrix. + var det:Number = -nrmX*acy*abz + acx*nrmY*abz + nrmX*aby*acz - abx*nrmY*acz - acx*aby*nrmZ + abx*acy*nrmZ; + var ima:Number = (-nrmY*acz + acy*nrmZ)/det; + var imb:Number = (nrmX*acz - acx*nrmZ)/det; + var imc:Number = (-nrmX*acy + acx*nrmY)/det; + var imd:Number = (nax*nrmY*acz - nrmX*nay*acz - nax*acy*nrmZ + acx*nay*nrmZ + nrmX*acy*naz - acx*nrmY*naz)/det; + var ime:Number = (nrmY*abz - aby*nrmZ)/det; + var imf:Number = (-nrmX*abz + abx*nrmZ)/det; + var img:Number = (nrmX*aby - abx*nrmY)/det; + var imh:Number = (nrmX*nay*abz - nax*nrmY*abz + nax*aby*nrmZ - abx*nay*nrmZ - nrmX*aby*naz + abx*nrmY*naz)/det; + var ma:Number = abu*ima + acu*ime; + var mb:Number = abu*imb + acu*imf; + var mc:Number = abu*imc + acu*img; + var md:Number = abu*imd + acu*imh + nau; + var me:Number = abv*ima + acv*ime; + var mf:Number = abv*imb + acv*imf; + var mg:Number = abv*imc + acv*img; + var mh:Number = abv*imd + acv*imh + nav; + // UV + res.uv = new Point(ma*point.x + mb*point.y + mc*point.z + md, me*point.x + mf*point.y + mg*point.z + mh); + } + + return res; + } else { + return null; + } + } + + /** + * @private + */ + alternativa3d function getVertexBuffer(attribute:int):VertexBuffer3D { + if (attribute < _attributesStreams.length) { + var stream:VertexStream = _attributesStreams[attribute]; + return stream != null ? stream.buffer : null; + } else { + return null; + } + } + + /** + * @private + */ + alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + var vBuffer:VertexStream = (VertexAttributes.POSITION < _attributesStreams.length) ? _attributesStreams[VertexAttributes.POSITION] : null; + if (vBuffer == null) { + throw new Error("Cannot calculate BoundBox without data."); + } + var offset:int = _attributesOffsets[VertexAttributes.POSITION]; + var numMappings:int = vBuffer.attributes.length; + var data:ByteArray = vBuffer.data; + + for (var i:int = 0; i < _numVertices; i++) { + data.position = 4*(numMappings*i + offset); + var vx:Number = data.readFloat(); + var vy:Number = data.readFloat(); + var vz:Number = data.readFloat(); + var x:Number, y:Number, z:Number; + if (transform != null) { + x = transform.a*vx + transform.b*vy + transform.c*vz + transform.d; + y = transform.e*vx + transform.f*vy + transform.g*vz + transform.h; + z = transform.i*vx + transform.j*vy + transform.k*vz + transform.l; + } else { + x = vx; + y = vy; + z = vz; + } + if (x < boundBox.minX) boundBox.minX = x; + if (x > boundBox.maxX) boundBox.maxX = x; + if (y < boundBox.minY) boundBox.minY = y; + if (y > boundBox.maxY) boundBox.maxY = y; + if (z < boundBox.minZ) boundBox.minZ = z; + if (z > boundBox.maxZ) boundBox.maxZ = z; + } + } + + } +} diff --git a/src/alternativa/engine3d/resources/TextureResource.as b/src/alternativa/engine3d/resources/TextureResource.as new file mode 100644 index 0000000..b02dfd9 --- /dev/null +++ b/src/alternativa/engine3d/resources/TextureResource.as @@ -0,0 +1,56 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.resources { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Resource; + + import flash.display3D.textures.TextureBase; + + use namespace alternativa3d; + + /** + * Base resource for texture resources, that can be uploaded into the video memory. + * BitmapTextureResource and ATFTextureResource allows user + * to upload textures into the video memory from BitmapData and ATF format accordingly. + * ExternalTextureResource should be used with TexturesLoader, + * that uploads textures from files and automatically puts them into the video memory. + * Size of texture must be power of two (e.g., 256х256, 128*512, 256* 32). + * @see alternativa.engine3d.resources.BitmapTextureResource + * @see alternativa.engine3d.resources.ATFTextureResource + * @see alternativa.engine3d.resources.ExternalTextureResource + */ + public class TextureResource extends Resource { + + /** + * @private + */ + alternativa3d var _texture:TextureBase; + + /** + * @inheritDoc + */ + override public function get isUploaded():Boolean { + return _texture != null; + } + + /** + * @inheritDoc + */ + override public function dispose():void { + if (_texture != null) { + _texture.dispose(); + _texture = null; + } + } + + } +} diff --git a/src/alternativa/engine3d/resources/WireGeometry.as b/src/alternativa/engine3d/resources/WireGeometry.as new file mode 100644 index 0000000..516fad3 --- /dev/null +++ b/src/alternativa/engine3d/resources/WireGeometry.as @@ -0,0 +1,189 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.resources { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.core.Resource; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.materials.ShaderProgram; + + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DVertexBufferFormat; + import flash.display3D.IndexBuffer3D; + import flash.display3D.VertexBuffer3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class WireGeometry extends Resource { + + private const MAX_VERTICES_COUNT:uint = 65500; + private const VERTEX_STRIDE:uint = 7; + + alternativa3d var vertexBuffers:Vector.; + alternativa3d var indexBuffers:Vector.; + private var nTriangles:Vector.; + private var vertices:Vector.>; + private var indices:Vector.>; + + // Current set of pairs vertex-buffer + index-buffer, that has a place for writing. + private var currentSetIndex:int = 0; + private var currentSetVertexOffset:uint = 0; + + public function WireGeometry() { + vertexBuffers = new Vector.(1); + indexBuffers = new Vector.(1); + clear(); + } + + override public function upload(context3D:Context3D):void { + for (var i:int = 0; i <= currentSetIndex; i++) { + if (vertexBuffers[i] != null) { + vertexBuffers[i].dispose(); + } + if (indexBuffers[i] != null) { + indexBuffers[i].dispose(); + } + if (nTriangles[i] > 0) { + var verts:Vector. = vertices[i]; + var inds:Vector. = indices[i]; + var vBuffer:VertexBuffer3D = vertexBuffers[i] = context3D.createVertexBuffer(verts.length/VERTEX_STRIDE, VERTEX_STRIDE); + vBuffer.uploadFromVector(verts, 0, verts.length/VERTEX_STRIDE); + var iBuffer:IndexBuffer3D = indexBuffers[i] = context3D.createIndexBuffer(inds.length); + iBuffer.uploadFromVector(inds, 0, inds.length); + } + } + } + + override public function dispose():void { + for (var i:int = 0; i <= currentSetIndex; i++) { + if (vertexBuffers[i] != null) { + vertexBuffers[i].dispose(); + vertexBuffers[i] = null; + } + if (indexBuffers[i] != null) { + indexBuffers[i].dispose(); + indexBuffers[i] = null; + } + } + } + + override public function get isUploaded():Boolean { + for (var i:int = 0; i <= currentSetIndex; i++) { + if (vertexBuffers[i] == null) { + return false; + } + if (indexBuffers[i] == null) { + return false; + } + } + return true; + } + + public function clear():void { + dispose(); + vertices = new Vector.>(); + indices = new Vector.>(); + vertices[0] = new Vector.(); + indices[0] = new Vector.(); + nTriangles = new Vector.(1); + currentSetVertexOffset = 0; + } + + alternativa3d function updateBoundBox(boundBox:BoundBox, transform:Transform3D = null):void { + for (var i:int = 0, count:int = vertices.length; i < count; i++) { + for (var j:int = 0, vcount:int = vertices[i].length; j < vcount; j += VERTEX_STRIDE) { + var verts:Vector. = vertices[i]; + var vx:Number = verts[j]; + var vy:Number = verts[int(j + 1)]; + var vz:Number = verts[int(j + 2)]; + var x:Number, y:Number, z:Number; + if (transform != null) { + x = transform.a*vx + transform.b*vy + transform.c*vz + transform.d; + y = transform.e*vx + transform.f*vy + transform.g*vz + transform.h; + z = transform.i*vx + transform.j*vy + transform.k*vz + transform.l; + } else { + x = vx; + y = vy; + z = vz; + } + if (x < boundBox.minX) boundBox.minX = x; + if (x > boundBox.maxX) boundBox.maxX = x; + if (y < boundBox.minY) boundBox.minY = y; + if (y > boundBox.maxY) boundBox.maxY = y; + if (z < boundBox.minZ) boundBox.minZ = z; + if (z > boundBox.maxZ) boundBox.maxZ = z; + } + } + } + + alternativa3d function getDrawUnits(camera:Camera3D, color:Vector., thickness:Number, object:Object3D, shader:ShaderProgram):void { + for (var i:int = 0; i <= currentSetIndex; i++) { + var iBuffer:IndexBuffer3D = indexBuffers[i]; + var vBuffer:VertexBuffer3D = vertexBuffers[i]; + if (iBuffer != null && vBuffer != null) { + var drawUnit:DrawUnit = camera.renderer.createDrawUnit(object, shader.program, iBuffer, 0, nTriangles[i], shader); + drawUnit.setVertexBufferAt(0, vBuffer, 0, Context3DVertexBufferFormat.FLOAT_4); + drawUnit.setVertexBufferAt(1, vBuffer, 4, Context3DVertexBufferFormat.FLOAT_3); + drawUnit.setVertexConstantsFromNumbers(0, 0, 1, -1, 0.000001); + drawUnit.setVertexConstantsFromNumbers(1, -1/camera.focalLength, 0, camera.nearClipping, thickness); + drawUnit.setVertexConstantsFromTransform(2, object.localToCameraTransform); + drawUnit.setProjectionConstants(camera, 5); + drawUnit.setFragmentConstantsFromNumbers(0, color[0], color[1], color[2], color[3]); + if (color[3] < 1) { + drawUnit.blendSource = Context3DBlendFactor.SOURCE_ALPHA; + drawUnit.blendDestination = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA; + camera.renderer.addDrawUnit(drawUnit, Renderer.TRANSPARENT_SORT); + } else { + camera.renderer.addDrawUnit(drawUnit, Renderer.OPAQUE); + } + } + } + } + + alternativa3d function addLine(v1x:Number, v1y:Number, v1z:Number, v2x:Number, v2y:Number, v2z:Number):void { + var currentVertices:Vector. = vertices[currentSetIndex]; + var currentIndices:Vector. = indices[currentSetIndex]; + var verticesCount:uint = currentVertices.length/VERTEX_STRIDE; + + if (verticesCount > (MAX_VERTICES_COUNT - 4)) { + // Limit of vertices has been reached + currentSetVertexOffset = 0; + currentSetIndex++; + nTriangles[currentSetIndex] = 0; + currentVertices = vertices[currentSetIndex] = new Vector.(); + currentIndices = indices[currentSetIndex] = new Vector.(); + vertexBuffers.length = currentSetIndex + 1; + indexBuffers.length = currentSetIndex + 1; + } else { + nTriangles[currentSetIndex] += 2; + } + currentVertices.push( + v1x, v1y, v1z, 0.5, v2x, v2y, v2z, + v2x, v2y, v2z, -0.5, v1x, v1y, v1z, + v1x, v1y, v1z, -0.5, v2x, v2y, v2z, + v2x, v2y, v2z, 0.5, v1x, v1y, v1z + ); + currentIndices.push(currentSetVertexOffset, currentSetVertexOffset + 1, currentSetVertexOffset + 2, + currentSetVertexOffset + 3, currentSetVertexOffset + 2, currentSetVertexOffset + 1); + currentSetVertexOffset += 4; + } + + } +} diff --git a/src/alternativa/engine3d/shadows/DirectionalLightShadow.as b/src/alternativa/engine3d/shadows/DirectionalLightShadow.as new file mode 100644 index 0000000..f4765f1 --- /dev/null +++ b/src/alternativa/engine3d/shadows/DirectionalLightShadow.as @@ -0,0 +1,879 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.shadows { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Debug; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Renderer; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.materials.Material; + import alternativa.engine3d.materials.ShaderProgram; + import alternativa.engine3d.materials.TextureMaterial; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Joint; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.objects.Skin; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.ExternalTextureResource; + import alternativa.engine3d.resources.Geometry; + import alternativa.engine3d.resources.TextureResource; + + import flash.display3D.Context3D; + import flash.display3D.Context3DProgramType; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.VertexBuffer3D; + import flash.display3D.textures.Texture; + import flash.geom.Rectangle; + import flash.geom.Vector3D; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * Class of shadow, that is created by one source of light(DirectionalLight). Shadow is rendered in fixed volume. + * For binding of shadow to light source you need: + * 1) to set DirectionalLightShadow as a value of property shadow of light source; + * 2) to add Object3D to corresponding list, using the method addCaster(). + * + * @see #addCaster() + * @see alternativa.engine3d.lights.DirectionalLight#shadow + * @see #farBoundPosition + */ + public class DirectionalLightShadow extends Shadow { + + private var renderer:Renderer = new Renderer(); + + /** + * Debug mode. + */ + public var debug:Boolean = false; + + /** + * Degree of correcting offset of shadow map space. It need for getting rid of self-shadowing artifacts. + */ + public var biasMultiplier:Number = 0.99; + + // TODO: implement property parent + + /** + * Coordinate X of center of shadow rendering area. Relative to the center are specified such properties as: + * width, height, nearBoundPosition, farBoundPosition. + * @see #width + * @see #height + * @see #nearBoundPosition + * @see #farBoundPosition + */ + public var centerX:Number = 0; + + /** + * Coordinate Y of center of shadow rendering area. Relative to the center are specified such properties as: + * width, height, nearBoundPosition, farBoundPosition. + * @see #width + * @see #height + * @see #nearBoundPosition + * @see #farBoundPosition + */ + public var centerY:Number = 0; + + /** + * Coordinate Z of center of shadow rendering area. Relative to the center are specified such properties as: + * width, height, nearBoundPosition, farBoundPosition. + * @see #width + * @see #height + * @see #nearBoundPosition + * @see #farBoundPosition + */ + public var centerZ:Number = 0; + + /** + * Width of shadow area (basics of bounbox). + * @see #centerX + * @see #centerY + * @see #centerZ + */ + public var width:Number; + + /** + * Length of shadow area (basics of bounbox). + * @see #centerX + * @see #centerY + * @see #centerZ + */ + public var height:Number; + + /** + * Near clipping bound of calculation of shadow area. + * Shadow map essentially similar to z-buffer: distance from light source to shadow + * casting place is coded by pixel color. So, properties nearBoundPosition + * and farBoundPosition in some ways are analogues of Camera3D.farClipping + * and Camera3D.nearclipping. The greater the range between nearBoundPosition + * and farBoundPosition , the rougher the coordinates of the pixel shader + * will be determined. Shadow area, that is not included into this range would not be drawn. + * Value is measured from center of shadow, that is set by properties: centerX, + * centerY, centerZ. + * @see #centerX + * @see #centerY + * @see #centerZ + */ + public var nearBoundPosition:Number = 0; + + /** + * Far clipping bound of calculation of shadow area. + * Shadow map essentially similar to z-buffer: distance from light source to shadow + * casting place is coded by pixel color. So, properties nearBoundPosition + * and farBoundPosition in some ways are analogues of Camera3D.farClipping + * and Camera3D.nearclipping. The greater the range between nearBoundPosition + * and farBoundPosition , the rougher the coordinates of the pixel shader + * will be determined. Shadow area, that is not included into this range would not be drawn. + * Value is measured from center of shadow, that is set by properties centerX, + * centerY, centerZ. + * @see #centerX + * @see #centerY + * @see #centerZ + */ + public var farBoundPosition:Number = 0; + + // TODO: implement property rotation + + private var _casters:Vector. = new Vector.(); + private var actualCasters:Vector. = new Vector.(); + + private var programs:Dictionary = new Dictionary(); + private var cachedContext:Context3D; + private var shadowMap:Texture; + private var _mapSize:int; + + // TODO: to understand the correctness of offset setting in shadowmap units. (It is possible that it is incorrect after the clipping on zone on edges). + private var _pcfOffset:Number; + + /** + * Enable/disable automatic calculation of shadow zone parameters on specified bound-box at shadowBoundBox property. + */ + public var calculateParametersByVolume:Boolean = false; + public var volume:BoundBox = null; + + // TODO: implement special shader for display of shadowmap in debug (black-and-white). + private var debugTexture:ExternalTextureResource = new ExternalTextureResource("debug"); + private var debugMaterial:TextureMaterial; + private var emptyLightVector:Vector. = new Vector.(); + + private var debugPlane:Mesh; + + // Matrix of projection from light source to context. + private var cameraToShadowMapContextProjection:Transform3D = new Transform3D(); + // Matrix of projection from light source to shadowmap texture. + private var cameraToShadowMapUVProjection:Transform3D = new Transform3D(); + // Auxiliary matrix for transfer of object to shadowmap. + private var objectToShadowMapTransform:Transform3D = new Transform3D(); + // Auxiliary matrix for transfer from global space to local space of light source. + private var globalToLightTransform:Transform3D = new Transform3D(); + + private var tempBounds:BoundBox = new BoundBox(); + private var rect:Rectangle = new Rectangle(); + private var tmpPoints:Vector. = Vector.([ + new Vector3D(), new Vector3D(), new Vector3D(), new Vector3D() + ]); + private var localTmpPoints:Vector. = Vector.([ + new Vector3D(), new Vector3D(), new Vector3D(), new Vector3D() + ]); + + /** + * Create an instance of DirectionalLightShadow. + * @param width Width of area, that will dispay shadow. + * @param height Lenght of area, that will display shadow. + * @param nearBoundPosition Near bound of cut of shadow calculation area. + * @param farBoundPosition Far bound of cut of shadow calculation area. + * @param mapSize Size of shadow map. Must be a power of two. + * @param pcfOffset Mitigation of shadow bounds. + */ + public function DirectionalLightShadow(width:Number, height:Number, nearBoundPosition:Number, farBoundPosition:Number, mapSize:int = 512, pcfOffset:Number = 0) { + this.width = width; + this.height = height; + this.nearBoundPosition = nearBoundPosition; + this.farBoundPosition = farBoundPosition; + + if (mapSize < 2) { + throw new ArgumentError("Map size cannot be less than 2."); + } else if (mapSize > 2048) { + throw new ArgumentError("Map size exceeds maximum value 2048."); + } + if ((Math.log(mapSize)/Math.LN2 % 1) != 0) { + throw new ArgumentError("Map size must be power of two."); + } + this._mapSize = mapSize; + + this._pcfOffset = pcfOffset; + this.type = _pcfOffset > 0 ? "S" : "s"; + + vertexShadowProcedure = getVShader(); + fragmentShadowProcedure = _pcfOffset > 0 ? getFShaderPCF() : getFShader(); + + debugMaterial = new TextureMaterial(debugTexture); + debugMaterial.alphaThreshold = 1.1; + debugMaterial.opaquePass = false; + debugMaterial.alpha = 0.7; + } + + private function createDebugPlane(material:Material, context:Context3D):Mesh { + var mesh:Mesh = new Mesh(); + var geometry:Geometry = new Geometry(4); + mesh.geometry = geometry; + + var attributes:Array = new Array(); + attributes[0] = VertexAttributes.POSITION; + attributes[1] = VertexAttributes.POSITION; + attributes[2] = VertexAttributes.POSITION; + attributes[3] = VertexAttributes.TEXCOORDS[0]; + attributes[4] = VertexAttributes.TEXCOORDS[0]; + geometry.addVertexStream(attributes); + + geometry.setAttributeValues(VertexAttributes.POSITION, Vector.([-0.5, -0.5, 0, -0.5, 0.5, 0, 0.5, 0.5, 0, 0.5, -0.5, 0])); + geometry.setAttributeValues(VertexAttributes.TEXCOORDS[0], Vector.([0, 0, 0, 1, 1, 1, 1, 0])); + + geometry.indices = Vector.([0, 1, 3, 2, 3, 1, 0, 3, 1, 2, 1, 3]); + + mesh.addSurface(material, 0, 4); + geometry.upload(context); + + return mesh; + } + + /** + * @private + */ + override alternativa3d function process(camera:Camera3D):void { + var i:int; + var object:Object3D; + // Clipping of casters, that have shadows which are invisible. + var numActualCasters:int = 0; + for (i = 0; i < _casters.length; i++) { + object = _casters[i]; + + var visible:Boolean = object.visible; + var parent:Object3D = object._parent; + while (visible && parent != null) { + visible = parent.visible; + parent = parent._parent; + } + if (visible) { + actualCasters[numActualCasters++] = object; + } + } + + if (camera.context3D != cachedContext) { + // Processing of changing of context. + programs = new Dictionary(); + shadowMap = null; + debugPlane = null; + cachedContext = camera.context3D; + } + + var frustumMinX:Number; + var frustumMaxX:Number; + var frustumMinY:Number; + var frustumMaxY:Number; + var frustumMinZ:Number; + var frustumMaxZ:Number; + + globalToLightTransform.combine(_light.cameraToLocalTransform, camera.globalToLocalTransform); + + // 2 - calculate boundaries of shadow frustum. + if (calculateParametersByVolume) { + updateParametersByVolume(); + } + var cx:Number = centerX*globalToLightTransform.a + centerY*globalToLightTransform.b + centerZ*globalToLightTransform.c + globalToLightTransform.d; + var cy:Number = centerX*globalToLightTransform.e + centerY*globalToLightTransform.f + centerZ*globalToLightTransform.g + globalToLightTransform.h; + var cz:Number = centerX*globalToLightTransform.i + centerY*globalToLightTransform.j + centerZ*globalToLightTransform.k + globalToLightTransform.l; + + // Size of pixel in light source. + var wPSize:Number = width/_mapSize; + var hPSize:Number = height/_mapSize; + // Round coordinates of center to integer pixels. + cx = Math.round(cx/wPSize)*wPSize; + cy = Math.round(cy/hPSize)*hPSize; + // TODO: implement rounding among the z-axis too + + frustumMinX = cx - width*0.5; + frustumMaxX = cx + width*0.5; + frustumMinY = cy - height*0.5; + frustumMaxY = cy + height*0.5; + + frustumMinZ = cz + nearBoundPosition; + frustumMaxZ = cz + farBoundPosition; + + // Calculation of projection matrices to shadowmap in context. + var correction:Number = (_mapSize - 2)/_mapSize; + // Calculate projection matrix. + cameraToShadowMapContextProjection.a = 2/(frustumMaxX - frustumMinX)*correction; + cameraToShadowMapContextProjection.b = 0; + cameraToShadowMapContextProjection.c = 0; + cameraToShadowMapContextProjection.e = 0; + cameraToShadowMapContextProjection.f = -2/(frustumMaxY - frustumMinY)*correction; + cameraToShadowMapContextProjection.g = 0; + cameraToShadowMapContextProjection.h = 0; + cameraToShadowMapContextProjection.i = 0; + cameraToShadowMapContextProjection.j = 0; + cameraToShadowMapContextProjection.k = 1 / (frustumMaxZ - frustumMinZ); + cameraToShadowMapContextProjection.d = (-0.5 * (frustumMaxX + frustumMinX) * cameraToShadowMapContextProjection.a); + cameraToShadowMapContextProjection.h = (-0.5 * (frustumMaxY + frustumMinY) * cameraToShadowMapContextProjection.f); + cameraToShadowMapContextProjection.l = -frustumMinZ / (frustumMaxZ - frustumMinZ); + + cameraToShadowMapUVProjection.copy(cameraToShadowMapContextProjection); + cameraToShadowMapUVProjection.a = 1 / ((frustumMaxX - frustumMinX)) * correction; + cameraToShadowMapUVProjection.f = 1 / ((frustumMaxY - frustumMinY)) * correction; + cameraToShadowMapUVProjection.d = 0.5 - (0.5 * (frustumMaxX + frustumMinX) * cameraToShadowMapUVProjection.a); + cameraToShadowMapUVProjection.h = 0.5 - (0.5 * (frustumMaxY + frustumMinY) * cameraToShadowMapUVProjection.f); + + cameraToShadowMapContextProjection.prepend(_light.cameraToLocalTransform); + cameraToShadowMapUVProjection.prepend(_light.cameraToLocalTransform); + + // Calculation of transfer matrix to space of shadowmap texture. + for (i = 0; i < numActualCasters; i++) { + object = actualCasters[i]; + // 4- Collect drawcalls for caster and its child objects. + collectDraws(camera.context3D, object); + } + + // Rendering of drawacalls to atlas. + if (shadowMap == null) { + shadowMap = camera.context3D.createTexture(_mapSize, _mapSize, Context3DTextureFormat.BGRA, true); + debugTexture._texture = shadowMap; + } + camera.context3D.setRenderToTexture(shadowMap, true); + camera.context3D.clear(1, 0, 0, 0.3); + + renderer.camera = camera; + + rect.x = 1; + rect.y = 1; + rect.width = _mapSize - 2; + rect.height = _mapSize - 2; + camera.context3D.setScissorRectangle(rect); + + renderer.render(camera.context3D); + + camera.context3D.setScissorRectangle(null); + + camera.context3D.setRenderToBackBuffer(); + + if (debug) { + if (numActualCasters > 0) { + if (debugPlane == null) { + debugPlane = createDebugPlane(debugMaterial, camera.context3D); + } + // Form transformation matrix for debugPlane + debugPlane.transform.compose((frustumMinX + frustumMaxX) / 2, (frustumMinY + frustumMaxY) / 2, frustumMinZ, 0, 0, 0, (frustumMaxX - frustumMinX), (frustumMaxY - frustumMinY), 1); + debugPlane.localToCameraTransform.combine(_light.localToCameraTransform, debugPlane.transform); + + // Draw + var debugSurface:Surface = debugPlane._surfaces[0]; + debugSurface.material.collectDraws(camera, debugSurface, debugPlane.geometry, emptyLightVector, 0, -1); + + // Form transformation matrix for debugPlane + debugPlane.transform.compose((frustumMinX + frustumMaxX) / 2, (frustumMinY + frustumMaxY) / 2, frustumMaxZ, 0, 0, 0, (frustumMaxX - frustumMinX), (frustumMaxY - frustumMinY), 1); + debugPlane.localToCameraTransform.combine(_light.localToCameraTransform, debugPlane.transform); + debugSurface.material.collectDraws(camera, debugSurface, debugPlane.geometry, emptyLightVector, 0, -1); + } + + tempBounds.minX = frustumMinX; + tempBounds.maxX = frustumMaxX; + tempBounds.minY = frustumMinY; + tempBounds.maxY = frustumMaxY; + tempBounds.minZ = frustumMinZ; + tempBounds.maxZ = frustumMaxZ; + Debug.drawBoundBox(camera, tempBounds, _light.localToCameraTransform, 0xe1cd27); + } + } + + private function updateParametersByVolume():void { +// globalToLightTransform.combine(_light.cameraToLocalTransform, camera.globalToLocalTransform); + + if (volume != null) { + // converts boundbox to point. + tmpPoints[0].x = tmpPoints[2].x = tmpPoints[3].x = volume.minX; + tmpPoints[1].x = volume.maxX; + tmpPoints[2].y = volume.minY; + tmpPoints[0].y = tmpPoints[1].y = tmpPoints[3].y = volume.maxY; + tmpPoints[0].z = tmpPoints[1].z = tmpPoints[2].z = volume.minZ; + tmpPoints[3].z = volume.maxZ; + + var i:int; + var tmpPoint:Vector3D; + var x:Number; + var y:Number; + var z:Number; + var localX:Number; + var localY:Number; + var localZ:Number; + + // converts points to local space. + tmpPoint = tmpPoints[0]; + x = tmpPoint.x; + y = tmpPoint.y; + z = tmpPoint.z; + localX = x*globalToLightTransform.a + y*globalToLightTransform.b + z*globalToLightTransform.c + globalToLightTransform.d; + localY = x*globalToLightTransform.e + y*globalToLightTransform.f + z*globalToLightTransform.g + globalToLightTransform.h; + localZ = x*globalToLightTransform.i + y*globalToLightTransform.j + z*globalToLightTransform.k + globalToLightTransform.l; + tempBounds.minX = localX; + tempBounds.maxX = localX; + tempBounds.minY = localY; + tempBounds.maxY = localY; + tempBounds.minZ = localZ; + tempBounds.maxZ = localZ; + tmpPoint = localTmpPoints[0]; + tmpPoint.x = localX; + tmpPoint.y = localY; + tmpPoint.z = localZ; + + for (i = 1; i<4; i++){ + tmpPoint = tmpPoints[i]; + x = tmpPoint.x; + y = tmpPoint.y; + z = tmpPoint.z; + localX = x*globalToLightTransform.a + y*globalToLightTransform.b + z*globalToLightTransform.c + globalToLightTransform.d; + localY = x*globalToLightTransform.e + y*globalToLightTransform.f + z*globalToLightTransform.g + globalToLightTransform.h; + localZ = x*globalToLightTransform.i + y*globalToLightTransform.j + z*globalToLightTransform.k + globalToLightTransform.l; + + // Find maximums and minimums and put them to local boundbox. + if (tempBounds.minX > localX)tempBounds.minX = localX; + if (tempBounds.maxX < localX)tempBounds.maxX = localX; + if (tempBounds.minY > localY)tempBounds.minY = localY; + if (tempBounds.maxY < localY)tempBounds.maxY = localY; + if (tempBounds.minZ > localZ)tempBounds.minZ = localZ; + if (tempBounds.maxZ < localZ)tempBounds.maxZ = localZ; + tmpPoint = localTmpPoints[i]; + tmpPoint.x = localX; + tmpPoint.y = localY; + tmpPoint.z = localZ; + } + + // Find last four points and maximums/minimums of them. + var localTmpPoint0:Vector3D = localTmpPoints[0]; + var localTmpPoint1:Vector3D = localTmpPoints[1]; + var localTmpPoint2:Vector3D = localTmpPoints[2]; + var localTmpPoint3:Vector3D = localTmpPoints[3]; + //7 + localX = localTmpPoint2.x + localTmpPoint3.x - localTmpPoint0.x; + localY = localTmpPoint2.y + localTmpPoint3.y - localTmpPoint0.y; + localZ = localTmpPoint2.z + localTmpPoint3.z - localTmpPoint0.z; + if (tempBounds.minX > localX)tempBounds.minX = localX; + if (tempBounds.maxX < localX)tempBounds.maxX = localX; + if (tempBounds.minY > localY)tempBounds.minY = localY; + if (tempBounds.maxY < localY)tempBounds.maxY = localY; + if (tempBounds.minZ > localZ)tempBounds.minZ = localZ; + if (tempBounds.maxZ < localZ)tempBounds.maxZ = localZ; + + //5 + localX = localTmpPoint3.x + localTmpPoint1.x - localTmpPoint0.x; + localY = localTmpPoint3.y + localTmpPoint1.y - localTmpPoint0.y; + localZ = localTmpPoint3.z + localTmpPoint1.z - localTmpPoint0.z; + if (tempBounds.minX > localX)tempBounds.minX = localX; + if (tempBounds.maxX < localX)tempBounds.maxX = localX; + if (tempBounds.minY > localY)tempBounds.minY = localY; + if (tempBounds.maxY < localY)tempBounds.maxY = localY; + if (tempBounds.minZ > localZ)tempBounds.minZ = localZ; + if (tempBounds.maxZ < localZ)tempBounds.maxZ = localZ; + + //6 + localX = localTmpPoint2.x + localTmpPoint1.x - localTmpPoint0.x; + localY = localTmpPoint2.y + localTmpPoint1.y - localTmpPoint0.y; + localZ = localTmpPoint2.z + localTmpPoint1.z - localTmpPoint0.z; + if (tempBounds.minX > localX)tempBounds.minX = localX; + if (tempBounds.maxX < localX)tempBounds.maxX = localX; + if (tempBounds.minY > localY)tempBounds.minY = localY; + if (tempBounds.maxY < localY)tempBounds.maxY = localY; + if (tempBounds.minZ > localZ)tempBounds.minZ = localZ; + if (tempBounds.maxZ < localZ)tempBounds.maxZ = localZ; + + //4 + localX = localX + localTmpPoint3.x - localTmpPoint0.x; + localY = localY + localTmpPoint3.y - localTmpPoint0.y; + localZ = localZ + localTmpPoint3.z - localTmpPoint0.z; + if (tempBounds.minX > localX)tempBounds.minX = localX; + if (tempBounds.maxX < localX)tempBounds.maxX = localX; + if (tempBounds.minY > localY)tempBounds.minY = localY; + if (tempBounds.maxY < localY)tempBounds.maxY = localY; + if (tempBounds.minZ > localZ)tempBounds.minZ = localZ; + if (tempBounds.maxZ < localZ)tempBounds.maxZ = localZ; + + //Calculate parameters, depending on the boundbox. + width = tempBounds.maxX - tempBounds.minX; + height = tempBounds.maxY - tempBounds.minY; + nearBoundPosition = (tempBounds.minZ - tempBounds.maxZ)/2; + farBoundPosition = -nearBoundPosition; + + centerX = (volume.minX + volume.maxX)/2; + centerY = (volume.minY + volume.maxY)/2; + centerZ = (volume.minZ + volume.maxZ)/2; + } + } + + private function getProgram(transformProcedure:Procedure, programListByTransformProcedure:Vector., context:Context3D, alphaTest:Boolean, useDiffuseAlpha:Boolean):ShaderProgram { + var key:int = (alphaTest ? (useDiffuseAlpha ? 1 : 2) : 0); + var program:ShaderProgram = programListByTransformProcedure[key]; + + if (program == null) { + var vLinker:Linker = new Linker(Context3DProgramType.VERTEX); + var fLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + + var positionVar:String = "aPosition"; + vLinker.declareVariable(positionVar, VariableType.ATTRIBUTE); + + if (alphaTest) { + vLinker.addProcedure(passUVProcedure); + } + + if (transformProcedure != null) { + var newPosVar:String = "tTransformedPosition"; + vLinker.declareVariable(newPosVar); + vLinker.addProcedure(transformProcedure, positionVar); + vLinker.setOutputParams(transformProcedure, newPosVar); + positionVar = newPosVar; + } + + + var proc:Procedure = Procedure.compileFromArray([ + "#c3=cScale", + "#v0=vDistance", + "m34 t0.xyz, i0, c0", + "mov t0.w, c3.w", + "mul v0, t0, c3.x", + "mov o0, t0" + ]); + proc.assignVariableName(VariableType.CONSTANT, 0, "cTransform", 3); + vLinker.addProcedure(proc, positionVar); + + if (alphaTest) { + if (useDiffuseAlpha) { + fLinker.addProcedure(diffuseAlphaTestProcedure); + } else { + fLinker.addProcedure(opacityAlphaTestProcedure); + } + } + fLinker.addProcedure(Procedure.compileFromArray([ + "#v0=vDistance", + "#c0=cConstants", + "mov t0.xy, v0.zz", + "frc t0.y, v0.z", + "sub t0.x, v0.z, t0.y", + "mul t0.x, t0.x, c0.x", + "mov t0.z, c0.z", + "mov t0.w, c0.w", + "mov o0, t0" + ])); + program = new ShaderProgram(vLinker, fLinker); + fLinker.varyings = vLinker.varyings; + programListByTransformProcedure[key] = program; + program.upload(context); + } + return program; + } + + /** + * @private + * Procedure for passing of UV coordinates to fragment shader. + */ + static private const passUVProcedure:Procedure = new Procedure(["#v0=vUV", "#a0=aUV", "mov v0, a0"], "passUVProcedure"); + + // diffuse alpha test + private static const diffuseAlphaTestProcedure:Procedure = new Procedure([ + "#v0=vUV", + "#s0=sTexture", + "#c0=cThresholdAlpha", + "tex t0, v0, s0 <2d, linear,repeat, miplinear>", + "mul t0.w, t0.w, c0.w", + "sub t0.w, t0.w, c0.x", + "kil t0.w", + ], "diffuseAlphaTestProcedure"); + + // opacity alpha test + private static const opacityAlphaTestProcedure:Procedure = new Procedure([ + "#v0=vUV", + "#s0=sTexture", + "#c0=cThresholdAlpha", + "tex t0, v0, s0 <2d, linear,repeat, miplinear>", + "mul t0.w, t0.x, c0.w", + "sub t0.w, t0.w, c0.x", + "kil t0.w"], "opacityAlphaTestProcedure"); + + // collectDraws for rendering to shadowmap. + private function collectDraws(context:Context3D, object:Object3D):void { + // alphaThreshold:Number, diffuse:TextureResource, opacity:TextureResource, materialAlpha:Number + var child:Object3D; + + var mesh:Mesh = object as Mesh; + if (mesh != null && mesh.geometry != null) { + var program:ShaderProgram; + var programListByTransformProcedure:Vector.; + var skin:Skin = mesh as Skin; + + if (skin != null) { + // Calculation of matrices of joints. + for (child = skin.childrenList; child != null; child = child.next) { + if (child.transformChanged) child.composeTransforms(); + // Write в localToGlobalTransform matrix of transfering to skin coordinates + child.localToGlobalTransform.copy(child.transform); + if (child is Joint) { + Joint(child).calculateTransform(); + } + skin.calculateJointsTransforms(child); + } + } + + // 1- calculation of transfer matrix from object to light source. + objectToShadowMapTransform.combine(cameraToShadowMapContextProjection, object.localToCameraTransform); + + for (var i:int = 0; i < mesh._surfacesLength; i++) { + // Form drawcall. + var surface:Surface = mesh._surfaces[i]; + if (surface.material == null) continue; + + var material:Material = surface.material; + var geometry:Geometry = mesh.geometry; + var alphaTest:Boolean; + var useDiffuseAlpha:Boolean; + var alphaThreshold:Number; + var materialAlpha:Number; + var diffuse:TextureResource; + var opacity:TextureResource; + var uvBuffer:VertexBuffer3D; + + if (material is TextureMaterial) { + alphaThreshold = TextureMaterial(material).alphaThreshold; + materialAlpha = TextureMaterial(material).alpha; + diffuse = TextureMaterial(material).diffuseMap; + opacity = TextureMaterial(material).opacityMap; + alphaTest = alphaThreshold > 0; + useDiffuseAlpha = TextureMaterial(material).opacityMap == null; + uvBuffer = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]); + if (uvBuffer == null) continue; + } else { + alphaTest = false; + useDiffuseAlpha = false; + } + + var positionBuffer:VertexBuffer3D = mesh.geometry.getVertexBuffer(VertexAttributes.POSITION); + if (positionBuffer == null) continue; + + if (skin != null) { + object.transformProcedure = skin.surfaceTransformProcedures[i]; + } + programListByTransformProcedure = programs[object.transformProcedure]; + if (programListByTransformProcedure == null) { + programListByTransformProcedure = new Vector.(3, true); + programs[object.transformProcedure] = programListByTransformProcedure; + } + program = getProgram(object.transformProcedure, programListByTransformProcedure, context, alphaTest, useDiffuseAlpha); + + var drawUnit:DrawUnit = renderer.createDrawUnit(object, program.program, mesh.geometry._indexBuffer, surface.indexBegin, surface.numTriangles, program); + + // Setting of buffers. + object.setTransformConstants(drawUnit, surface, program.vertexShader, null); + + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aPosition"), positionBuffer, mesh.geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + + if (alphaTest) { + drawUnit.setVertexBufferAt(program.vertexShader.getVariableIndex("aUV"), uvBuffer, geometry._attributesOffsets[VertexAttributes.TEXCOORDS[0]], VertexAttributes.FORMATS[VertexAttributes.TEXCOORDS[0]]); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cThresholdAlpha"), alphaThreshold, 0, 0, materialAlpha); + if (useDiffuseAlpha) { + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sTexture"), diffuse._texture); + } else { + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sTexture"), opacity._texture); + } + } + + // Setting of constants. + drawUnit.setVertexConstantsFromTransform(program.vertexShader.getVariableIndex("cTransform"), objectToShadowMapTransform); + drawUnit.setVertexConstantsFromNumbers(program.vertexShader.getVariableIndex("cScale"), 255, 0, 0, 1); + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("cConstants"), 1 / 255, 0, 0, 1); + + renderer.addDrawUnit(drawUnit, Renderer.OPAQUE); + } + } + for (child = object.childrenList; child != null; child = child.next) { + if (child.visible) collectDraws(context, child); + } + } + + //------------- ShadowMap Shader ---------- + + private static function getVShader():Procedure { + var shader:Procedure = Procedure.compileFromArray([ + "#v0=vSample", + "m34 v0.xyz, i0, c0", + "mov v0.w, i0.w" + ], "DirectionalShadowMapVertex"); + shader.assignVariableName(VariableType.CONSTANT, 0, "cUVProjection", 3); + return shader; + } + + private static function getFShader():Procedure { + var shaderArr:Array = [ + "#v0=vSample", + "#c0=cConstants", + "#c1=cDist", // 0, -max*10000, 10000 + "#s0=sShadowMap" + ]; + var line:int = 4; + shaderArr[line++] = "mov t0.zw, v0.zz"; + // Distance. + shaderArr[line++] = "tex t0.xy, v0, s0 <2d,clamp,near,nomip>"; + shaderArr[line++] = "dp3 t0.x, t0.xyz, c0.xyz"; + + // Clipping by distance. + shaderArr[line++] = "sub t0.y, c1.x, t0.z"; // maxDist - z + shaderArr[line++] = "mul t0.y, t0.y, c1.y"; // mul 10000 + shaderArr[line++] = "sat t0.xy, t0.xy"; + shaderArr[line++] = "mul t0.x, t0.x, t0.y"; + shaderArr[line++] = "sub o0, c1.z, t0.x"; + + return Procedure.compileFromArray(shaderArr, "DirectionalShadowMapFragment"); + } + + private static const pcfOffsetRegisters:Array = [ + "xx", "xy", "xz", "xw", + "yx", "yy", "yz", "yw", + "zx", "zy", "zz", "zw", + "wx", "wy", "wz", "ww" + ]; + private static const componentByIndex:Array = [ + "x", "y", "z", "w" + ]; + + private static function getFShaderPCF():Procedure { + var shaderArr:Array = [ + "#v0=vSample", + "#c0=cConstants", + "#c1=cPCFOffsets", + "#c2=cDist", + "#s0=sShadowMap" + ]; + var line:int = 5; + shaderArr[line++] = "mov t0.zw, v0.zz"; // put distance to t1.z + for (var i:int = 0; i < 16; i++) { + var column:int = i & 3; + + // Calculation of offset + shaderArr[line++] = "add t0.xy, v0.xy, c1." + pcfOffsetRegisters[i]; + // Distance. + shaderArr[line++] = "tex t0.xy, t0, s0 <2d,clamp,near,nomip>"; + shaderArr[line++] = "dp3 t1." + componentByIndex[column] + ", t0.xyz, c0.xyz"; // restore distance and calculate difference + if (column == 3) { + // Last item in string. + shaderArr[line++] = "sat t1, t1"; + shaderArr[line++] = "dp4 t2." + componentByIndex[int(i >> 2)] + ", t1, c0.w"; + } + } + shaderArr[line++] = "dp4 t0.x, t2, v0.w"; + + // Clipping by distance. + shaderArr[line++] = "sub t0.y, c2.x, t0.z"; // maxDist - z + shaderArr[line++] = "mul t0.y, t0.y, c2.y"; // mul 10000 + shaderArr[line++] = "sat t0.y, t0.y"; + shaderArr[line++] = "mul t0.x, t0.x, t0.y"; + shaderArr[line++] = "sub o0, c2.z, t0.x"; + + return Procedure.compileFromArray(shaderArr, "DirectionalShadowMapFragment"); + } + + /** + * @private + */ + alternativa3d override function setup(drawUnit:DrawUnit, vertexLinker:Linker, fragmentLinker:Linker, surface:Surface):void { + // Set transfer matrix to shadowmap. + objectToShadowMapTransform.combine(cameraToShadowMapUVProjection, surface.object.localToCameraTransform); + + drawUnit.setVertexConstantsFromTransform(vertexLinker.getVariableIndex("cUVProjection"), objectToShadowMapTransform); + // Set shadowmap. + drawUnit.setTextureAt(fragmentLinker.getVariableIndex("sShadowMap"), shadowMap); + // TODO: set multiplier more correct. It is possible that 65536 (resolution of the buffer depth). + // Set coefficients. + drawUnit.setFragmentConstantsFromNumbers(fragmentLinker.getVariableIndex("cConstants"), -255*10000, -10000, biasMultiplier*255*10000, 1/16); + if (_pcfOffset > 0) { + var offset1:Number = _pcfOffset/_mapSize; + var offset2:Number = offset1/3; + + drawUnit.setFragmentConstantsFromNumbers(fragmentLinker.getVariableIndex("cPCFOffsets"), -offset1, -offset2, offset2, offset1); + } + drawUnit.setFragmentConstantsFromNumbers(fragmentLinker.getVariableIndex("cDist"), 0.9999, 10000, 1); + } + + /** + * Adds given object to list of objects, that cast shadow. + * @param object Added object. + */ + public function addCaster(object:Object3D):void { + if (_casters.indexOf(object) < 0) { + _casters.push(object); + } + } + + /** + * Clears the list of objects, that cast shadow. + */ + public function clearCasters():void { + _casters.length = 0; + } + + /** + * Set resolution of shadow map. This property can get value of power of 2 (up to 2048). + */ + public function get mapSize():int { + return _mapSize; + } + + /** + * @private + */ + public function set mapSize(value:int):void { + if (value != _mapSize) { + this._mapSize = value; + if (value < 2) { + throw new ArgumentError("Map size cannot be less than 2."); + } else if (value > 2048) { + throw new ArgumentError("Map size exceeds maximum value 2048."); + } + if ((Math.log(value)/Math.LN2 % 1) != 0) { + throw new ArgumentError("Map size must be power of two."); + } + if (shadowMap != null) { + shadowMap.dispose(); + } + shadowMap = null; + } + } + + /** + * Offset of Percentage Closer Filtering. This way of filtering is used for mitigation of shadow bounds. + */ + public function get pcfOffset():Number { + return _pcfOffset; + } + + /** + * @private + */ + public function set pcfOffset(value:Number):void { + _pcfOffset = value; + type = _pcfOffset > 0 ? "S" : "s"; + fragmentShadowProcedure = _pcfOffset > 0 ? getFShaderPCF() : getFShader(); + } + + } +} diff --git a/src/alternativa/engine3d/shadows/DirectionalShadowRenderer.as b/src/alternativa/engine3d/shadows/DirectionalShadowRenderer.as new file mode 100644 index 0000000..ffdf234 --- /dev/null +++ b/src/alternativa/engine3d/shadows/DirectionalShadowRenderer.as @@ -0,0 +1,540 @@ +package alternativa.engine3d.shadows { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.lights.DirectionalLight; + import alternativa.engine3d.materials.ShaderProgram; + import alternativa.engine3d.materials.TextureMaterial; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.primitives.Box; + import alternativa.engine3d.resources.ExternalTextureResource; + import alternativa.engine3d.resources.TextureResource; + + import flash.display3D.Context3D; + import flash.display3D.Context3DProgramType; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.Context3DTriangleFace; + import flash.display3D.Program3D; + import flash.display3D.textures.Texture; + import flash.geom.Matrix3D; + import flash.geom.Vector3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class DirectionalShadowRenderer extends ShadowRenderer { + + public var offset:Vector3D = new Vector3D(); + + public var caster:Object3D; + + private var context:Context3D; + + private var shadowMap:Texture; + private var _worldSize:Number; + + private var light:DirectionalLight; + alternativa3d var globalToShadowMap:Matrix3D = new Matrix3D(); + + private var debugObject:Mesh; + public var debugMaterial:TextureMaterial = new TextureMaterial(); + private var debugTexture:TextureResource = new ExternalTextureResource("null"); +// private var debugTexture:TextureResource = new BitmapTextureResource(new BitmapData(4, 4, false, 0xFF0000)); + + private static const constants:Vector. = Vector.([ +// 255, 255*0.98, 100, 1 + 255, 255*0.96, 100, 1 + ]); + + private var pcfOffset:Number = 0; + private var pcfOffsets:Vector.; + + public function DirectionalShadowRenderer(context:Context3D, size:int, worldSize:Number, pcfSize:Number = 0) { + this.context = context; + this._worldSize = worldSize; + this.pcfOffset = pcfSize/worldSize/255; +// this.pcfOffset = pcfSize; + if (pcfOffset > 0) { + pcfOffsets = Vector.([ + -pcfOffset, -pcfOffset, 0, 1/4, + -pcfOffset, pcfOffset, 0, 1, + pcfOffset, -pcfOffset, 0, 1, + pcfOffset, pcfOffset, 0, 1 + ]); + } + this.shadowMap = context.createTexture(size, size, Context3DTextureFormat.BGRA, true); + debugTexture._texture = this.shadowMap; + debugMaterial.diffuseMap = debugTexture; + debugMaterial.alpha = 0.9; + // TODO: fix + debugMaterial.transparentPass = true; + debugMaterial.opaquePass = false; + debugMaterial.alphaThreshold = 1.1; + +// debugTexture.upload(context); + + debugObject = new Box(worldSize, worldSize, 1, 1, 1, 1, false, debugMaterial); + debugObject.geometry.upload(context); + } + + public function get worldSize():Number { + return _worldSize; + } + + public function set worldSize(value:Number):void { + _worldSize = value; + var newDebug:Mesh = new Box(_worldSize, _worldSize, 1, 1, 1, 1, false, debugMaterial); + newDebug.geometry.upload(context); + if (debugObject._parent != null) { + debugObject._parent.addChild(newDebug); + debugObject._parent.removeChild(debugObject); + } + debugObject = newDebug; + } + + private var _debug:Boolean = false; + public function setLight(value:DirectionalLight):void { + light = value; + if (_debug) { + light.addChild(debugObject); + } + } + + override public function get debug():Boolean { + return _debug; + } + + override public function set debug(value:Boolean):void { + _debug = value; + if (_debug) { + if (light != null) { + light.addChild(debugObject); + } + } else { + if (debugObject._parent != null) { + debugObject._parent.removeChild(debugObject); + } + } + } + + private static var matrix:Matrix3D = new Matrix3D(); + override alternativa3d function cullReciever(boundBox:BoundBox, object:Object3D):Boolean { + copyMatrixFromTransform(matrix, object.localToGlobalTransform); + matrix.append(this.globalToShadowMap); + return cullObjectImplementation(boundBox, matrix); + } + + private var lightProjectionMatrix:Matrix3D = new Matrix3D(); + private var uvMatrix:Matrix3D = new Matrix3D(); + private var center:Vector3D = new Vector3D(); + override public function update():void { + active = true; + var root:Object3D; + // Расчитываем матрицу объекта +// if (caster.transformChanged) { + caster.localToCameraTransform.compose(caster._x, caster._y, caster._z, caster._rotationX, caster._rotationY, caster._rotationZ, caster._scaleX, caster._scaleY, caster._scaleZ); +// } else { +// caster.localToCameraTransform.copy(caster.transform); +// } + root = caster; + while (root._parent != null) { + root = root._parent; +// if (root.transformChanged) { + root.localToGlobalTransform.compose(root._x, root._y, root._z, root._rotationX, root._rotationY, root._rotationZ, root._scaleX, root._scaleY, root._scaleZ); +// } + caster.localToCameraTransform.append(root.localToGlobalTransform); + } + + // Расчитываем матрицу лайта + light.localToGlobalTransform.compose(light._x, light._y, light._z, light._rotationX, light._rotationY, light._rotationZ, light._scaleX, light._scaleY, light._scaleZ); + root = light; + while (root._parent != null) { + root = root._parent; +// if (root.transformChanged) { + root.localToGlobalTransform.compose(root._x, root._y, root._z, root._rotationX, root._rotationY, root._rotationZ, root._scaleX, root._scaleY, root._scaleZ); +// } + light.localToGlobalTransform.append(root.localToGlobalTransform); + } + light.globalToLocalTransform.copy(light.localToGlobalTransform); + light.globalToLocalTransform.invert(); + + // Получаем матрицу перевода из объекта в лайт + caster.localToCameraTransform.append(light.globalToLocalTransform); + + // Расчет матрицы проецирования + var t:Transform3D = caster.localToCameraTransform; + center.x = t.a*offset.x + t.b*offset.y + t.c*offset.z + t.d; + center.y = t.e*offset.x + t.f*offset.y + t.g*offset.z + t.h; + center.z = t.i*offset.x + t.j*offset.y + t.k*offset.z + t.l; +// var center:Vector3D = new Vector3D(caster.localToCameraTransform.d, caster.localToCameraTransform.h, caster.localToCameraTransform.l); + + calculateShadowMapProjection(lightProjectionMatrix, uvMatrix, center, _worldSize, _worldSize, _worldSize); + copyMatrixFromTransform(globalToShadowMap, light.globalToLocalTransform); + globalToShadowMap.append(uvMatrix); + + debugObject.x = center.x; + debugObject.y = center.y; + debugObject.z = center.z - _worldSize/2; +// trace("center", center); + + debugMaterial.diffuseMap = null; + + // Рисуем в шедоумапу + context.setRenderToTexture(shadowMap, true, 0, 0); +// context.clear(1); + context.clear(1, 1, 1, 1); + cleanContext(context); + drawObjectToShadowMap(context, caster, light, lightProjectionMatrix); + context.setRenderToBackBuffer(); + cleanContext(context); + debugMaterial.diffuseMap = debugTexture; + } + + private static var transformToMatrixRawData:Vector. = new Vector.(16); + alternativa3d static function copyMatrixFromTransform(matrix:Matrix3D, transform:Transform3D):void { + transformToMatrixRawData[0] = transform.a; + transformToMatrixRawData[1] = transform.e; + transformToMatrixRawData[2] = transform.i; + transformToMatrixRawData[3] = 0; + transformToMatrixRawData[4] = transform.b; + transformToMatrixRawData[5] = transform.f; + transformToMatrixRawData[6] = transform.j; + transformToMatrixRawData[7] = 0; + transformToMatrixRawData[8] = transform.c; + transformToMatrixRawData[9] = transform.g; + transformToMatrixRawData[10] = transform.k; + transformToMatrixRawData[11] = 0; + transformToMatrixRawData[12] = transform.d; + transformToMatrixRawData[13] = transform.h; + transformToMatrixRawData[14] = transform.l; + transformToMatrixRawData[15] = 1; +// matrix.copyRawDataFrom(transformToMatrixRawData); + matrix.rawData = transformToMatrixRawData; + } + + alternativa3d static function drawObjectToShadowMap(context:Context3D, object:Object3D, light:DirectionalLight, projection:Matrix3D):void { + if (object is Mesh) { + drawMeshToShadowMap(context, Mesh(object), projection); + } + for (var child:Object3D = object.childrenList; child != null; child = child.next) { + if (child.visible && child.useShadow) { + if (child.transformChanged) child.composeTransforms(); + child.localToCameraTransform.combine(object.localToCameraTransform, child.transform); + drawObjectToShadowMap(context, child, light, projection); + } + } + } + + private static var drawProjection:Matrix3D = new Matrix3D(); + private static var directionalShadowMapProgram:Program3D; + private static function drawMeshToShadowMap(context:Context3D, mesh:Mesh, projection:Matrix3D):void { + if (mesh.geometry == null || mesh.geometry.numTriangles == 0 || !mesh.geometry.isUploaded) { + return; + } + + copyMatrixFromTransform(drawProjection, mesh.localToCameraTransform); + drawProjection.append(projection); + if (directionalShadowMapProgram == null) directionalShadowMapProgram = initMeshToShadowMapProgram(context); + context.setProgram(directionalShadowMapProgram); + + context.setVertexBufferAt(0, mesh.geometry.getVertexBuffer(VertexAttributes.POSITION), mesh.geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + + context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, drawProjection, true); + context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 4, Vector.([255, 0, 0, 1])); + context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.([1/255, 0, 0, 1])); + + context.setCulling(Context3DTriangleFace.BACK); + + for (var i:int = 0; i < mesh._surfacesLength; i++) { + var surface:Surface = mesh._surfaces[i]; + if (surface.material == null || !surface.material.canDrawInShadowMap) continue; + context.drawTriangles(mesh.geometry._indexBuffer, surface.indexBegin, surface.numTriangles); + } + context.setVertexBufferAt(0, null); + } + + private static function initMeshToShadowMapProgram(context3d:Context3D):Program3D { + var vLinker:Linker = new Linker(Context3DProgramType.VERTEX); + var fLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + var proc:Procedure = Procedure.compileFromArray([ + "#a0=a0", + "#c4=c4", + "#v0=v0", + "m44 t0, a0, c0", + "mul v0, t0, c4.x", + "mov o0, t0" + ]); + proc.assignVariableName(VariableType.CONSTANT, 0, "c0", 4); + vLinker.addProcedure(proc); + + fLinker.addProcedure(Procedure.compileFromArray([ + "#v0=v0", + "#c0=c0", + "mov t0.xy, v0.zz", + "frc t0.y, v0.z", + "sub t0.x, v0.z, t0.y", + "mul t0.x, t0.x, c0.x", + "mov t0.z, c0.z", + "mov t0.w, c0.w", + "mov o0, t0" + ])); + var program:Program3D = context3d.createProgram(); +// trace("VERTEX"); +// trace(A3DUtils.disassemble(vLinker.getByteCode())); +// trace("FRAGMENT"); +// trace(A3DUtils.disassemble(fLinker.getByteCode())); + fLinker.varyings = vLinker.varyings; + vLinker.link(); + fLinker.link(); + program.upload(vLinker.data, fLinker.data); + return program; + } + + // должен быть заполнен нулями + private var rawData:Vector. = new Vector.(16); + private function calculateShadowMapProjection(matrix:Matrix3D, uvMatrix:Matrix3D, offset:Vector3D, width:Number, height:Number, length:Number):void { + var halfW:Number = width/2; + var halfH:Number = height/2; + var halfL:Number = length/2; + var frustumMinX:Number = offset.x - halfW; + var frustumMaxX:Number = offset.x + halfW; + var frustumMinY:Number = offset.y - halfH; + var frustumMaxY:Number = offset.y + halfH; + var frustumMinZ:Number = offset.z - halfL; + var frustumMaxZ:Number = offset.z + halfL; + + // Считаем матрицу проецирования + rawData[0] = 2/(frustumMaxX - frustumMinX); + rawData[5] = 2/(frustumMaxY - frustumMinY); + rawData[10]= 1/(frustumMaxZ - frustumMinZ); + rawData[12] = (-0.5 * (frustumMaxX + frustumMinX) * rawData[0]); + rawData[13] = (-0.5 * (frustumMaxY + frustumMinY) * rawData[5]); + rawData[14]= -frustumMinZ/(frustumMaxZ - frustumMinZ); + rawData[15]= 1; + matrix.rawData = rawData; + + rawData[0] = 1/((frustumMaxX - frustumMinX)); +// if (useSingle) { +// rawData[5] = 1/((frustumMaxY - frustumMinY)); +// } else { + rawData[5] = -1/((frustumMaxY - frustumMinY)); +// } + rawData[12] = 0.5 - (0.5 * (frustumMaxX + frustumMinX) * rawData[0]); + rawData[13] = 0.5 - (0.5 * (frustumMaxY + frustumMinY) * rawData[5]); + uvMatrix.rawData = rawData; + } + +/* + private static const fullVShader:Procedure = initFullVShader(); + private static function initFullVShader():Procedure { + var shader:Procedure = Procedure.compileFromArray([ + "m44 o0, a0, c0", + // Координата вершины в локальном пространстве + "m44 v0, a0, c4", + ]); + shader.assignVariableName(VariableType.ATTRIBUTE, 0, "aPosition"); + shader.assignVariableName(VariableType.CONSTANT, 0, "cPROJ", 4); + shader.assignVariableName(VariableType.CONSTANT, 4, "cTOSHADOW", 4); + shader.assignVariableName(VariableType.VARYING, 0, "vSHADOWSAMPLE"); + return shader; + } +*/ + private static function initVShader(index:int):Procedure { + var shader:Procedure = Procedure.compileFromArray([ + "m44 v0, a0, c0" + ]); + shader.assignVariableName(VariableType.ATTRIBUTE, 0, "aPosition"); + shader.assignVariableName(VariableType.CONSTANT, 0, index + "cTOSHADOW", 4); + shader.assignVariableName(VariableType.VARYING, 0, index + "vSHADOWSAMPLE"); + return shader; + } + private static function initFShader(mult:Boolean, usePCF:Boolean, index:int, grayScale:Boolean = false):Procedure { + var i:int; + var line:int = 0; + var shaderArr:Array = []; + var numPass:uint = (usePCF) ? 4 : 1; + for (i = 0; i < numPass; i++) { + // Расстояние + shaderArr[line++] = "mov t0.w, v0.z"; + shaderArr[line++] = "mul t0.w, t0.w, c4.y"; // bias [0.99] * 255 + + if (usePCF) { + // Добавляем смещение + shaderArr[line++] = "mul t1, c" + (i + 6).toString() + ", t0.w"; + shaderArr[line++] = "add t1, v0, t1"; + shaderArr[line++] = "tex t1, t1, s0 <2d,clamp,near,nomip>"; + } else { + shaderArr[line++] = "tex t1, v0, s0 <2d,clamp,near,nomip>"; + } + + // Восстанавливаем расстояние + shaderArr[line++] = "mul t1.w, t1.x, c4.x"; // * 255 + shaderArr[line++] = "add t1.w, t1.w, t1.y"; + + // Перекрытие тенью + shaderArr[line++] = "sub t2.z, t1.w, t0.w"; + shaderArr[line++] = "mul t2.z, t2.z, c4.z"; // smooth [10000] + shaderArr[line++] = "sat t2.z, t2.z"; + + // Добавляем маску и прозрачность, затем sat + if (grayScale) { + shaderArr[line++] = "add t2, t2.zzzz, t1.zzzz"; // маска тени + } else { + shaderArr[line++] = "add t2.z, t2.z, t1.z"; // маска тени + shaderArr[line++] = "add t2, t2.zzzz, c5"; // цвет тени + } + shaderArr[line++] = "sat t2, t2"; + + if (usePCF) { + if (i == 0) { + shaderArr[line++] = "mov t3, t2"; + } else { + shaderArr[line++] = "add t3, t3, t2"; + } + } + } + if (usePCF) { + shaderArr[line++] = "mul t2, t3, c6.w"; + } + if (grayScale) { + shaderArr[line++] = "mov o0.w, t2.x"; + } else { + if (mult) { + shaderArr[line++] = "mul t0.xyz, i0.xyz, t2.xyz"; + shaderArr[line++] = "mov t0.w, i0.w"; + shaderArr[line++] = "mov o0, t0"; + } else { + shaderArr[line++] = "mov o0, t2"; + } + } + var shader:Procedure = Procedure.compileFromArray(shaderArr, "DirectionalShadowMap"); + shader.assignVariableName(VariableType.VARYING, 0, index + "vSHADOWSAMPLE"); + shader.assignVariableName(VariableType.CONSTANT, 4, index + "cConstants", 1); + if (!grayScale) shader.assignVariableName(VariableType.CONSTANT, 5, index + "cShadowColor", 1); + if (usePCF) { + for (i = 0; i < numPass; i++) { + shader.assignVariableName(VariableType.CONSTANT, i + 6, "cDPCF" + i.toString(), 1); + } + } + shader.assignVariableName(VariableType.SAMPLER, 0, index + "sSHADOWMAP"); + return shader; + } + + override public function getVShader(index:int = 0):Procedure { + return initVShader(index); + } + override public function getFShader(index:int = 0):Procedure { + return initFShader(false, (pcfOffset > 0), index); + } +// override public function getMultFShader():Procedure { +// return initFShader(true, (pcfOffset > 0), 0); +// } +// override public function getMultVShader():Procedure { +// return initVShader(0); +// } + + override public function getFIntensityShader():Procedure { + return initFShader(false, (pcfOffset > 0), 0, true); + } + + private static const objectToShadowMap:Matrix3D = new Matrix3D(); + private static const localToGlobal:Transform3D = new Transform3D(); + private static const vector:Vector. = new Vector.(16, false); + override public function applyShader(drawUnit:DrawUnit, program:ShaderProgram, object:Object3D, camera:Camera3D, index:int = 0):void { + // Считаем матрицу перевода в лайт из объекта + localToGlobal.combine(camera.localToGlobalTransform, object.localToCameraTransform); + copyMatrixFromTransform(objectToShadowMap, localToGlobal); + objectToShadowMap.append(globalToShadowMap); + objectToShadowMap.copyRawDataTo(vector, 0, true); +// objectToShadowMap.transpose(); + +// drawUnit.setVertexConstantsFromVector(program.vertexShader.getVariableIndex(index + "cTOSHADOW"), objectToShadowMap.rawData, 4) + drawUnit.setVertexConstantsFromVector(program.vertexShader.getVariableIndex(index + "cTOSHADOW"), vector, 4) + drawUnit.setFragmentConstantsFromVector(program.fragmentShader.getVariableIndex(index + "cConstants"), constants, 1); + if (program.fragmentShader.containsVariable(index + "cShadowColor")) { +// drawUnit.setFragmentConstantsFromVector(program.fragmentShader.getVariableIndex(index + "cShadowColor"), camera.ambient, 1); + // В дальнейшем яркость тени увеличтся в два раза + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex(index + "cShadowColor"), camera.ambient[0]/2, camera.ambient[1]/2, camera.ambient[2]/2, 1); + } + + if (pcfOffset > 0) { +// destination.addFragmentConstantSet(program.fragmentShader.getVariableIndex(index + "cPCF0"), pcfOffsets, pcfOffsets.length/4); + drawUnit.setFragmentConstantsFromVector(program.fragmentShader.getVariableIndex("cDPCF0"), pcfOffsets, pcfOffsets.length/4); + } + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex(index + "sSHADOWMAP"), shadowMap); + } + +// override public function getTextureIndex(fLinker:Linker):int { +// return fLinker.getVariableIndex("sSHADOWMAP"); +// } + +// private static var program:ShaderProgram; +// private static var programPCF:ShaderProgram; +// private static function initMeshProgram(context:Context3D, usePCF:Boolean):ShaderProgram { +// var vLinker:Linker = new Linker(Context3DProgramType.VERTEX); +// vLinker.addProcedure(fullVShader); +// +// var fLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); +// if (usePCF) { +// fLinker.addProcedure(pcfFShader); +// } else { +// fLinker.addProcedure(fShader); +// } +// +// vLinker.setOppositeLinker(fLinker); +// fLinker.setOppositeLinker(vLinker); +// +//// trace("[VERTEX]"); +//// trace(AgalUtils.disassemble(vLinker.getByteCode())); +//// trace("[FRAGMENT]"); +//// trace(AgalUtils.disassemble(fLinker.getByteCode())); +// +// var result:ShaderProgram; +// if (usePCF) { +// programPCF = new ShaderProgram(vLinker, fLinker); +// result = programPCF; +// } else { +// program = new ShaderProgram(vLinker, fLinker); +// result = program; +// } +// return result; +// } + +// override public function drawShadow(mesh:Mesh, camera:Camera3D, texture:Texture):void { +// var context3d:Context3D = camera.view._context3d; +// +// var linkedProgram:ShaderProgram; +// if (pcfOffset > 0) { +// linkedProgram = (programPCF == null) ? initMeshProgram(context3d, true) : programPCF; +// } else { +// linkedProgram = (program == null) ? initMeshProgram(context3d, false) : program; +// } +// var vLinker:Linker = linkedProgram.vLinker; +// var fLinker:Linker = linkedProgram.fLinker; +// context3d.setProgram(linkedProgram.program); +// +// context3d.setVertexBufferAt(vLinker.getVariableIndex("aPOSITION"), mesh.geometry.vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3); +// context3d.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, vLinker.getVariableIndex("cPROJ"), mesh.projectionMatrix, true); +// applyShader(context3d, linkedProgram, mesh, camera); +// context3d.setVertexBufferAt(1, null); +// +// context3d.setCulling(Context3DTriangleFace.FRONT); +// context3d.drawTriangles(mesh.geometry.indexBuffer, 0, mesh.geometry.numTriangles); +// +// context3d.setVertexBufferAt(vLinker.getVariableIndex("aPOSITION"), null); +// context.setTextureAt(getTextureIndex(fLinker), texture); +// } + + } +} diff --git a/src/alternativa/engine3d/shadows/OmniShadowRenderer.as b/src/alternativa/engine3d/shadows/OmniShadowRenderer.as new file mode 100644 index 0000000..270ac8f --- /dev/null +++ b/src/alternativa/engine3d/shadows/OmniShadowRenderer.as @@ -0,0 +1,798 @@ +package alternativa.engine3d.shadows { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.core.View; + import alternativa.engine3d.lights.OmniLight; + import alternativa.engine3d.materials.FillMaterial; + import alternativa.engine3d.materials.ShaderProgram; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.primitives.GeoSphere; + import alternativa.engine3d.resources.Geometry; + + import flash.display3D.Context3D; + import flash.display3D.Context3DProgramType; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.Context3DTriangleFace; + import flash.display3D.Program3D; + import flash.display3D.textures.CubeTexture; + import flash.geom.Vector3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class OmniShadowRenderer extends ShadowRenderer { + +// [Embed("geosphere.A3D", mimeType="application/octet-stream")] private static const ModelClass:Class; + private static const debugGeometry:Geometry = createDebugGeometry(); + private static function createDebugGeometry():Geometry { +// var parser:ParserA3D = new ParserA3D(); +// parser.parse(new ModelClass()); +// var mesh:Mesh = Mesh(parser.getObjectByName("sphere")); +// return mesh.geometry; + var geo:GeoSphere = new GeoSphere(0.5, 4); + return geo.geometry; + } + + private var caster:Object3D; + private var casterBounds:BoundBox = new BoundBox(); + + private var pcfOffset:Number = 0; + + private var context:Context3D; + + public var omnies:Vector.; + + private var shadowMap:CubeTexture; + private var shadowMapSize:int; + private var cameras:Vector. = new Vector.(); + private var clearBits:uint = 0xFF; + + private var currentOmni:OmniLight = new OmniLight(0, 0, 0); + + private static const constants:Vector. = Vector.([ + 255, 0.97, 10000, 1/255 + ]); + private static const offset:Number = 0.005; + private static const pcfOffsets:Vector. = Vector.([ + -offset, -offset, -offset, 1/8, + -offset, -offset, offset, 1, + -offset, offset, -offset, 1, + -offset, offset, offset, 1, + offset, -offset, -offset, 1, + offset, -offset, offset, 1, + offset, offset, -offset, 1, + offset, offset, offset, 1, + ]); + + public function OmniShadowRenderer(context:Context3D, size:int, pcfSize:Number = 0) { + this.context = context; + this.pcfOffset = pcfSize; + shadowMapSize = size; + shadowMap = context.createCubeTexture(size, Context3DTextureFormat.BGRA, true); + debugGeometry.upload(context); + + for (var i:int = 0; i < 6; i++) { + var cam:Camera3D = new Camera3D(1, 100); + cam.fov = 1.910633237; + cam.view = new View(size, size); + cameras[i] = cam; + } + // Left + cameras[1].rotationY = -Math.PI/2; + cameras[1].scaleY = -1; + // Right + cameras[0].rotationY = Math.PI/2; + cameras[0].scaleY = -1; + // Back + cameras[3].rotationX = -Math.PI/2; + cameras[3].rotationZ = Math.PI; + cameras[3].scaleX = -1; + // Front + cameras[2].rotationX = -Math.PI/2; + cameras[2].scaleY = -1; + // Bottom + cameras[5].rotationX = Math.PI; + cameras[5].scaleX = -1; + // Top + cameras[4].rotationX = 0; + cameras[4].scaleY = -1; + } + + alternativa3d override function cullReciever(boundBox:BoundBox, object:Object3D):Boolean { +// tempBounds.reset(); +// object.localToCameraTransform.copy(object.localToGlobalTransform); +// StaticShadowRenderer.calculateBoundBox(tempBounds, object, false); + var bounds:BoundBox = object.boundBox; + object.globalToLocalTransform.copy(object.localToGlobalTransform); + object.globalToLocalTransform.invert(); + var inverseMatrix:Transform3D = object.globalToLocalTransform; + +// trace(object.scaleX, object.scaleY, object.scaleZ); + var ox:Number = inverseMatrix.a*currentOmni._x + inverseMatrix.b*currentOmni._y + inverseMatrix.c*currentOmni._z + inverseMatrix.d; + var oy:Number = inverseMatrix.e*currentOmni._x + inverseMatrix.f*currentOmni._y + inverseMatrix.g*currentOmni._z + inverseMatrix.h; + var oz:Number = inverseMatrix.i*currentOmni._x + inverseMatrix.j*currentOmni._y + inverseMatrix.k*currentOmni._z + inverseMatrix.l; + var radius:Number = currentOmni.attenuationEnd; + if (ox + radius > bounds.minX && ox - radius < bounds.maxX && oy + radius > bounds.minY && oy - radius < bounds.maxY && oz + radius > bounds.minZ && oz - radius < bounds.maxZ) { + return true; + } + return false; + } + + alternativa3d override function get needMultiplyBlend():Boolean { + return true; + } + + public var debugObject:Mesh = new Mesh(); + // TODO: repair +// private var debugMaterial:OmniShadowRendererDebugMaterial = new OmniShadowRendererDebugMaterial(); + private var debugMaterial:Object; + + public function setCaster(object:Object3D):void { + caster = object; + object.localToCameraTransform.identity(); + StaticShadowRenderer.calculateBoundBox(casterBounds, object); + + debugObject.geometry = debugGeometry; +// debugObject.addSurface(debugMaterial, 0, debugGeometry.numTriangles); + debugObject.addSurface(new FillMaterial(0xFFFFFF), 0, debugGeometry.numTriangles); + debugObject.scaleX = 400; + debugObject.scaleY = 400; + debugObject.scaleZ = 400; + } + + private var culledOmnies:Vector. = new Vector.(); + + private var influences:Vector. = new Vector.(); + private static const inverseMatrix:Transform3D = new Transform3D(); + +// private static const omniLocalCoords:Vector. = new Vector.(3); + + override public function update():void { + // Расчет матрицы объекта + caster.localToGlobalTransform.compose(caster._x, caster._y, caster._z, caster._rotationX, caster._rotationY, caster._rotationZ, caster._scaleX, caster._scaleY, caster._scaleZ); + var root:Object3D = caster; + while (root._parent != null) { + root = root._parent; + root.localToGlobalTransform.compose(root._x, root._y, root._z, root._rotationX, root._rotationY, root._rotationZ, root._scaleX, root._scaleY, root._scaleZ); + caster.localToGlobalTransform.append(root.localToGlobalTransform); + } + // Расчет матрицы перевода в объект + caster.globalToLocalTransform.copy(caster.localToGlobalTransform); + caster.globalToLocalTransform.invert(); + +/** +// // Вычисление множителя масштаба +// caster.inverseCameraMatrix.transformVectors(sIn, sOut); +// var dx:Number = sOut[0] - sOut[3]; +// var dy:Number = sOut[1] - sOut[4]; +// var dz:Number = sOut[2] - sOut[5]; +// var scale:Number = Math.sqrt(dx*dx + dy*dy + dz*dz);*/ + +// var selectedOmni:OmniLight; +// var selectedOmniInfluence:Number = -1; + var influenceSum:Number = 0; + + var omni:OmniLight; + + culledOmnies.length = 0; + influences.length = 0; + // Куллинг источников света и нахождение основного + for each (omni in omnies) { + // Вычисление глобальной позиции омника + inverseMatrix.identity(); + var parent:Object3D = omni._parent; + if (parent != null) { + parent.localToGlobalTransform.compose(parent._x, parent._y, parent._z, parent._rotationX, parent._rotationY, parent._rotationZ, parent._scaleX, parent._scaleY, parent._scaleZ); + root = parent; + while (root._parent != null) { + if (root == caster || parent == caster) { + throw new Error("Caster can not be parent of light"); + } + root = root._parent; + root.localToGlobalTransform.compose(root._x, root._y, root._z, root._rotationX, root._rotationY, root._rotationZ, root._scaleX, root._scaleY, root._scaleZ); + parent.localToGlobalTransform.append(root.localToGlobalTransform); + } + inverseMatrix.append(parent.localToGlobalTransform); + } + inverseMatrix.append(caster.globalToLocalTransform); + + var ox:Number = inverseMatrix.a*omni._x + inverseMatrix.b*omni._y + inverseMatrix.c*omni._z + inverseMatrix.d; + var oy:Number = inverseMatrix.e*omni._x + inverseMatrix.f*omni._y + inverseMatrix.g*omni._z + inverseMatrix.h; + var oz:Number = inverseMatrix.i*omni._x + inverseMatrix.j*omni._y + inverseMatrix.k*omni._z + inverseMatrix.l; + + // Использовать описывающий баунд-бокс объекта + // Куллинг + if (ox + omni.attenuationEnd > casterBounds.minX && ox - omni.attenuationEnd < casterBounds.maxX && oy + omni.attenuationEnd > casterBounds.minY && oy - omni.attenuationEnd < casterBounds.maxY && oz + omni.attenuationEnd > casterBounds.minZ && oz - omni.attenuationEnd < casterBounds.maxZ) { + // В зоне действия источника + // Считаем степень влияния + var d:Number = Math.sqrt(ox*ox + oy*oy + oz*oz)/omni.attenuationEnd - 0.1; + var influence:Number; + if (d > 1) { + influence = 0; + } else { + influence = omni.intensity*calcBrightness(omni.color) * (1 - d); + } +// if (influence > selectedOmniInfluence) { +// selectedOmni = omni; +// selectedOmniInfluence = influence; +// } + influenceSum += influence; + influences.push(influence); + culledOmnies.push(omni); + } + } + debugMaterial.texture = null; + + var i:int; + var surface:uint; + var drawed:int = 0; +/** if (selectedOmni == null || influenceSum <= 0) {*/ + if (culledOmnies.length == 0 || influenceSum <= 0) { + // Ни один источник не влияет + for (i = 0; i < 6; i++) { + surface = 1 << i; + if (clearBits & surface) { + context.setRenderToTexture(shadowMap, true, 0, i); +// context.clear(1); + context.clear(1, 1, 1, 1); +// trace("clear", i); + clearBits &= ~surface; + } + } +// trace("INVISIBLE"); + } else { + currentOmni._x = 0; + currentOmni._y = 0; + currentOmni._z = 0; + currentOmni.attenuationEnd = 0; + for (i = 0; i < culledOmnies.length; i++) { + var weight:Number = influences[i]/influenceSum; + omni = culledOmnies[i]; + // Считаем матрицу перевода в глобальное пространство из омника + omni.localToGlobalTransform.identity(); + omni.localToGlobalTransform.d = omni.x; + omni.localToGlobalTransform.h = omni.y; + omni.localToGlobalTransform.l = omni.z; + root = omni; + while (root._parent != null) { + root = root._parent; + if (root.transformChanged) root.composeTransforms(); + omni.localToGlobalTransform.append(root.transform); + } + currentOmni._x += omni.localToGlobalTransform.d*weight; + currentOmni._y += omni.localToGlobalTransform.h*weight; + currentOmni._z += omni.localToGlobalTransform.l*weight; + currentOmni.attenuationEnd += omni.attenuationEnd*weight; + } + currentOmni.localToGlobalTransform.identity(); + currentOmni.localToGlobalTransform.d = currentOmni._x; + currentOmni.localToGlobalTransform.h = currentOmni._y; + currentOmni.localToGlobalTransform.l = currentOmni._z; + +// constants[3] = 0.5*1/255; + constants[3] = 1.0/255; +/** // Расчитываем яркость тени +// var weight:Number = (selectedOmniInfluence > 0) ? 1 - (influenceSum - selectedOmniInfluence)/influenceSum : 0; +// trace(weight, influenceSum, selectedOmniInfluence); +// trace(weight); +// var weight:Number = 1; +// if (weight > 0) { +// constants[3] = (1 + (1 - weight)*5)/255; +// } else { +// constants[3] = 1/255; +// } +// // Считаем матрицу перевода в глобальное пространство из омника +// selectedOmni.cameraMatrix.identity(); +// selectedOmni.cameraMatrix.appendTranslation(selectedOmni.x, selectedOmni.y, selectedOmni.z); +//// selectedOmni.composeMatrix(); +// root = selectedOmni; +// while (root._parent != null) { +// root = root._parent; +// root.composeMatrix(); +// selectedOmni.cameraMatrix.append(root.cameraMatrix); +// } +//// // Матрица родителя уже посчитана +//// if (omni._parent != null) { +//// omni.cameraMatrix.append(omni._parent.cameraMatrix); +//// } +// selectedOmni.globalCoords[0] = 0; +// selectedOmni.globalCoords[1] = 0; +// selectedOmni.globalCoords[2] = 0; +// selectedOmni.cameraMatrix.transformVectors(selectedOmni.globalCoords, selectedOmni.globalCoords); */ + // Записываем параметры омника в константы + + debugObject.x = currentOmni._x; + debugObject.y = currentOmni._y; + debugObject.z = currentOmni._z; + + cleanContext(context); + for (i = 0; i < 6; i++) { + surface = 1 << i; + context.setRenderToTexture(shadowMap, true, 0, i); +// trace("SIDE:", i); + if (renderToOmniShadowMap(currentOmni, cameras[i])) { + drawed++; + clearBits |= surface; + } else { + if (clearBits & surface) { +// trace("clear", i); + context.clear(1, 1, 1, 1); +// context.clear(1, 1, 1, 1); + clearBits &= ~surface; + } + } + } +// trace("NUMSIDES:", drawed); + debugMaterial.texture = shadowMap; + } + context.setRenderToBackBuffer(); + cleanContext(context); + active = drawed > 0; + } + + private function calcBrightness(color:uint):Number { + var r:uint = color & 0xFF; + var g:uint = (color >> 8) & 0xFF; + var b:uint = (color >> 16) & 0xFF; + var result:uint = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); + return result/255; + } + +// private var axises:Vector. = Vector.([ +// 1, 0, 0, +// 0, 1, 0, +// ]); +// private var globalAxises:Vector. = new Vector.(6); + + public function renderToOmniShadowMap(omni:OmniLight, camera:Camera3D):Boolean { + camera.nearClipping = 1; + camera.farClipping = omni.attenuationEnd; + // Расчёт параметров проецирования + camera.calculateProjection(camera.view._width, camera.view._height); + + if (camera.transformChanged) camera.composeTransforms(); + // Считаем омник родительским объектом камеры + camera.localToGlobalTransform.combine(omni.localToGlobalTransform, camera.transform); + camera.globalToLocalTransform.copy(camera.localToGlobalTransform); + camera.globalToLocalTransform.invert(); + + caster.localToCameraTransform.compose(caster._x, caster._y, caster._z, caster._rotationX, caster._rotationY, caster._rotationZ, caster._scaleX, caster._scaleY, caster._scaleZ); + var root:Object3D = caster; + while (root._parent != null) { + root = root._parent; + if (root.transformChanged) root.composeTransforms(); + caster.localToCameraTransform.append(root.transform); + } + caster.localToCameraTransform.append(camera.globalToLocalTransform); + +/** if (pcfOffset > 0.1) { +// axises[0] = pcfOffset; +// // Считаем преобразования PCF +// camera.globalMatrix.transformVectors(axises, globalAxises); +// pcfOffsets[0] = -pcfOffset*globalAxises[0]; +// pcfOffsets[1] = -pcfOffset*globalAxises[1]; +// pcfOffsets[2] = -pcfOffset*globalAxises[2]; +// pcfOffsets[4] = pcfOffset*globalAxises[0]; +// pcfOffsets[5] = pcfOffset*globalAxises[1]; +// pcfOffsets[6] = pcfOffset*globalAxises[2]; +// pcfOffsets[8] = -pcfOffset*globalAxises[3]; +// pcfOffsets[9] = -pcfOffset*globalAxises[4]; +// pcfOffsets[10] = -pcfOffset*globalAxises[5]; +// pcfOffsets[12] = pcfOffset*globalAxises[3]; +// pcfOffsets[13] = pcfOffset*globalAxises[4]; +// pcfOffsets[14] = pcfOffset*globalAxises[5]; + } */ + + // Отрисовка в шедоумапу + if (cullingInCamera(caster, casterBounds)) { + context.clear(1, 1, 0, 1); + drawObjectToShadowMap(context, caster, camera); + return true; + } + return false; + } + + private static const points:Vector. = Vector.([ + new Vector3D(), new Vector3D(), new Vector3D(), new Vector3D(), + new Vector3D(), new Vector3D(), new Vector3D(), new Vector3D(), + new Vector3D(), new Vector3D(), new Vector3D(), new Vector3D(), + new Vector3D(), new Vector3D(), new Vector3D(), new Vector3D() + ]); + private static const boundVertices:Vector. = new Vector.(24); + alternativa3d function cullingInCamera(object:Object3D, objectBounds:BoundBox):Boolean { + var i:int; + var infront:Boolean; + var behind:Boolean; + // Заполнение + var point:Vector3D; + var bb:BoundBox = objectBounds; + point = points[0]; + point.x = bb.minX; + point.y = bb.minY; + point.z = bb.minZ; + point = points[1]; + point.x = bb.minX; + point.y = bb.minY; + point.z = bb.maxZ; + point = points[2]; + point.x = bb.minX; + point.y = bb.maxY; + point.z = bb.minZ; + point = points[3]; + point.x = bb.minX; + point.y = bb.maxY; + point.z = bb.maxZ; + point = points[4]; + point.x = bb.maxX; + point.y = bb.minY; + point.z = bb.minZ; + point = points[5]; + point.x = bb.maxX; + point.y = bb.minY; + point.z = bb.maxZ; + point = points[6]; + point.x = bb.maxX; + point.y = bb.maxY; + point.z = bb.minZ; + point = points[7]; + point.x = bb.maxX; + point.y = bb.maxY; + point.z = bb.maxZ; + // Коррекция под 90 градусов + var transform:Transform3D = object.localToCameraTransform; + for (i = 0; i < 8; i++) { + point = points[i]; + var x:Number = transform.a*point.x + transform.b*point.y + transform.c*point.z + transform.d; + var y:Number = transform.e*point.x + transform.f*point.y + transform.g*point.z + transform.h; + var z:Number = transform.i*point.x + transform.j*point.y + transform.k*point.z + transform.l; + var index:int = 3*i; + boundVertices[int(index++)] = x; + boundVertices[int(index++)] = y; + boundVertices[index] = z; + } + + // Куллинг + for (i = 0, infront = false, behind = false; i <= 21; i += 3) { + if (-boundVertices[i] < boundVertices[int(i + 2)]) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { +// trace("L", infront); + if (!infront) return false; + } + for (i = 0, infront = false, behind = false; i <= 21; i += 3) { + if (boundVertices[i] < boundVertices[int(i + 2)]) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { +// trace("R", infront); + if (!infront) return false; + } + for (i = 1, infront = false, behind = false; i <= 22; i += 3) { + if (-boundVertices[i] < boundVertices[int(i + 1)]) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { +// trace("U", infront); + if (!infront) return false; + } + for (i = 1, infront = false, behind = false; i <= 22; i += 3) { + if (boundVertices[i] < boundVertices[int(i + 1)]) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { +// trace("D", infront); + if (!infront) return false; + } + return true; + } + + alternativa3d static function drawObjectToShadowMap(context:Context3D, object:Object3D, camera:Camera3D):void { + if (object is Mesh) { + drawMeshToShadowMap(context, Mesh(object), camera); + } + for (var child:Object3D = object.childrenList; child != null; child = child.next) { + if (child.visible) { + if (child.transformChanged) child.composeTransforms(); + child.localToCameraTransform.combine(object.localToCameraTransform, child.transform); + drawObjectToShadowMap(context, child, camera); + } + } + } + + private static function copyRawFromTransform(raw:Vector., transform:Transform3D):void { + raw[0] = transform.a; + raw[1] = transform.b; + raw[2] = transform.c; + raw[3] = transform.d; + raw[4] = transform.e; + raw[5] = transform.f; + raw[6] = transform.g; + raw[7] = transform.h; + raw[8] = transform.i; + raw[9] = transform.j; + raw[10] = transform.k; + raw[11] = transform.l; + raw[12] = 0; + raw[13] = 0; + raw[14] = 0; + raw[15] = 1; + } + + private static var shadowMapProgram:Program3D; + private static var projectionVector:Vector. = new Vector.(16); + private static function drawMeshToShadowMap(context:Context3D, mesh:Mesh, camera:Camera3D):void { + if (mesh.geometry == null || mesh.geometry.numTriangles == 0 || !mesh.geometry.isUploaded) { + return; + } + + // TODO : update to new logic + if (shadowMapProgram == null) shadowMapProgram = initMeshToShadowMapProgram(context); + context.setProgram(shadowMapProgram); + + context.setVertexBufferAt(0, mesh.geometry.getVertexBuffer(VertexAttributes.POSITION), mesh.geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + + // TODO: uncomment +// camera.composeProjectionMatrix(projectionVector, 0, mesh.localToCameraTransform); + + context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 0, projectionVector, 4); + + copyRawFromTransform(projectionVector, mesh.localToCameraTransform); + + context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 4, projectionVector, 4); + + context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 8, Vector.([Math.sqrt(255)/camera.farClipping, 0, 0, 1])); + context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.([1/255, 0, 0, 1])); + + context.setCulling(Context3DTriangleFace.BACK); + for (var i:int = 0; i < mesh._surfacesLength; i++) { + var surface:Surface = mesh._surfaces[i]; + if (surface.material == null) continue; + context.drawTriangles(mesh.geometry._indexBuffer, surface.indexBegin, surface.numTriangles); + } + context.setVertexBufferAt(0, null); + } + + private static function initMeshToShadowMapProgram(context3d:Context3D):Program3D { + var vLinker:Linker = new Linker(Context3DProgramType.VERTEX); + var fLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + var proc:Procedure = Procedure.compileFromArray([ + "#a0=a0", + "#c8=c8", + "#v0=v0", + "m44 o0, a0, c0", + "m44 t0, a0, c4", + "mul t0, t0, c8.x", + "mov v0, t0" + ]); + proc.assignVariableName(VariableType.CONSTANT, 0, "c0", 4); + proc.assignVariableName(VariableType.CONSTANT, 4, "c4", 4); + vLinker.addProcedure(proc); + + fLinker.addProcedure(Procedure.compileFromArray([ + "#v0=v0", + "#c0=c0", + "mov t0.zw, c0.z \n", + "dp3 t1.w, v0.xyz, v0.xyz \n", + "frc t0.y, t1.w", + "sub t0.x, t1.w, t0.y", + "mul t0.x, t0.x, c0.x", + "mov o0, to" + ])); + var program:Program3D = context3d.createProgram(); +// trace("VERTEX"); +// trace(A3DUtils.disassemble(vLinker.getByteCode())); +// trace("FRAGMENT"); +// trace(A3DUtils.disassemble(fLinker.getByteCode())); + fLinker.varyings = vLinker.varyings; + vLinker.link(); + fLinker.link(); + program.upload(vLinker.data, fLinker.data); + return program; + } + + private static function initVShader():Procedure { + var shader:Procedure = Procedure.compileFromArray([ + // Координата вершины в глобальном пространстве + "m44 v0, a0, c0" + ]); + shader.assignVariableName(VariableType.ATTRIBUTE, 0, "aPosition"); + shader.assignVariableName(VariableType.CONSTANT, 0, "cGLOBALMATRIX", 4); + shader.assignVariableName(VariableType.VARYING, 0, "vPOSITION"); + return shader; + } + + private static function initFShader(mult:Boolean, usePCF:Boolean):Procedure { + var line:int = 0; + var shaderArr:Array = []; + var numPass:uint = (usePCF) ? 8 : 1; + for (var i:int = 0; i < numPass; i++) { + // Вектор от источника света к точке + shaderArr[line++] = "sub t0.xyz, v0.xyz, c4.xyz"; + + // Квадрат расстояния + shaderArr[line++] = "dp3 t0.w, t0.xyz, t0.xyz"; + shaderArr[line++] = "mul t0.w, t0.w, c4.w"; // * (255 / radius^2) + shaderArr[line++] = "mul t0.w, t0.w, c5.y"; // bias [0.95] + + // Квадрат расстояния из карты теней + shaderArr[line++] = "nrm t0.xyz, t0.xyz"; + + if (usePCF) { + shaderArr[line++] = "add t0.xyz, t0.xyz, c" + (i + 6).toString(); + } + + shaderArr[line++] = "tex t1, t0, s0 "; + shaderArr[line++] = "mov t3, t1"; + shaderArr[line++] = "mul t1.w, t1.x, c5.x"; // 255 + shaderArr[line++] = "add t1.w, t1.w, t1.y"; + + // Перекрытие тенью + shaderArr[line++] = "sub t2.z, t1.w, t0.w"; + shaderArr[line++] = "mul t2.z, t2.z, c5.z"; // smooth [10000] + shaderArr[line++] = "sat t2.z, t2.z"; + +// // Затухание тени по расстоянию + shaderArr[line++] = "mul t1.x, t0.w, c5.w"; // div 255 + shaderArr[line++] = "add t2.z, t2.z, t1.x"; + if (i == 0) { + shaderArr[line++] = "sat t2.x, t2.z"; + } else { + shaderArr[line++] = "sat t2.z, t2.z"; + shaderArr[line++] = "add t2.x, t2.x, t2.z"; + } + } + if (usePCF) { + shaderArr[line++] = "mul t2.x, t2.x, c6.w"; + } + if (mult) { + shaderArr.push("mul t0.xyz, i0.xyz, t2.x"); +// shaderArr.push("mul t0.xyz, t1.w, c5.w"); + shaderArr.push("mov t0.w, i0.w"); + shaderArr.push("mov o0, t0"); + } else { + shaderArr.push("mov o0, t2.xxxx"); + } + var shader:Procedure = Procedure.compileFromArray(shaderArr, "OmniShadowMap"); + shader.assignVariableName(VariableType.VARYING, 0, "vPOSITION"); + shader.assignVariableName(VariableType.CONSTANT, 4, "cOmni", 1); + shader.assignVariableName(VariableType.CONSTANT, 5, "cConstants", 1); + if (usePCF) { + shader.assignVariableName(VariableType.CONSTANT, 6, "cPCF0", 1); + shader.assignVariableName(VariableType.CONSTANT, 7, "cPCF1", 1); + shader.assignVariableName(VariableType.CONSTANT, 8, "cPCF2", 1); + shader.assignVariableName(VariableType.CONSTANT, 9, "cPCF3", 1); + shader.assignVariableName(VariableType.CONSTANT, 10, "cPCF4", 1); + shader.assignVariableName(VariableType.CONSTANT, 11, "cPCF5", 1); + shader.assignVariableName(VariableType.CONSTANT, 12, "cPCF6", 1); + shader.assignVariableName(VariableType.CONSTANT, 13, "cPCF7", 1); + } + shader.assignVariableName(VariableType.SAMPLER, 0, "sCUBE"); + return shader; + } + + override public function getVShader(index:int = 0):Procedure { + return initVShader(); + } + + override public function getFShader(index:int = 0):Procedure { + return initFShader(false, pcfOffset > 0); + } + + private static const globalMatrix:Transform3D = new Transform3D(); + override public function applyShader(drawUnit:DrawUnit, program:ShaderProgram, object:Object3D, camera:Camera3D, index:int = 0):void { + var fLinker:Linker = program.fragmentShader; + + globalMatrix.combine(camera.localToGlobalTransform, object.localToCameraTransform); + + var mIndex:int = program.vertexShader.getVariableIndex("cGLOBALMATRIX"); + drawUnit.setVertexConstantsFromNumbers(mIndex, globalMatrix.a, globalMatrix.b, globalMatrix.c, globalMatrix.d); + drawUnit.setVertexConstantsFromNumbers(mIndex+1, globalMatrix.e, globalMatrix.f, globalMatrix.g, globalMatrix.h); + drawUnit.setVertexConstantsFromNumbers(mIndex+2, globalMatrix.i, globalMatrix.j, globalMatrix.k, globalMatrix.l); + drawUnit.setVertexConstantsFromNumbers(mIndex+3, 0, 0, 0, 1); + +// destination.addFragmentConstantSet(fLinker.getVariableIndex("cOmni"), omniPos, 1); + drawUnit.setFragmentConstantsFromNumbers(fLinker.getVariableIndex("cOmni"), currentOmni._x, currentOmni._y, currentOmni._z, 255/currentOmni.attenuationEnd/currentOmni.attenuationEnd); + drawUnit.setFragmentConstantsFromVector(fLinker.getVariableIndex("cConstants"), constants, 1); + if (pcfOffset > 0) { + drawUnit.setVertexConstantsFromVector(fLinker.getVariableIndex("cPCF0"), pcfOffsets, 8); + } + drawUnit.setTextureAt(fLinker.getVariableIndex("sCUBE"), shadowMap); + } + +// private static var program:LinkedProgram; +// private static var programPCF:LinkedProgram; +// private static function initMeshProgram(context:Context3D, usePCF:Boolean):LinkedProgram { +// var vLinker:Linker = new Linker(Context3DProgramType.VERTEX); +// vLinker.addShader(vShader); +// +// var fLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); +// if (usePCF) { +// fLinker.addShader(pcfFShader); +// } else { +// fLinker.addShader(fShader); +// } +// +// vLinker.setOppositeLinker(fLinker); +// fLinker.setOppositeLinker(vLinker); +// +// trace("[VERTEX]"); +// trace(AgalUtils.disassemble(vLinker.getByteCode())); +// trace("[FRAGMENT]"); +// trace(AgalUtils.disassemble(fLinker.getByteCode())); +// +// var result:LinkedProgram; +// if (usePCF) { +// programPCF = new LinkedProgram(); +// result = programPCF; +// } else { +// program = new LinkedProgram(); +// result = program; +// } +// result.vLinker = vLinker; +// result.fLinker = fLinker; +// result.program = context.createProgram(); +// result.program.upload(vLinker.getByteCode(), fLinker.getByteCode()); +// +// return result; +// } +// +// override public function drawShadow(mesh:Mesh, camera:Camera3D, texture:Texture):void { +// var context3d:Context3D = camera.view._context3d; +// +// var linkedProgram:LinkedProgram; +// if (pcfOffset > 0) { +// linkedProgram = (programPCF == null) ? initMeshProgram(context3d, true) : programPCF; +// } else { +// linkedProgram = (program == null) ? initMeshProgram(context3d, false) : program; +// } +// var vLinker:Linker = linkedProgram.vLinker; +// var fLinker:Linker = linkedProgram.fLinker; +// context3d.setProgram(linkedProgram.program); +// +// context3d.setVertexBufferAt(vLinker.getVariableIndex("aPOSITION"), mesh.geometry.vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3); +// context3d.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, vLinker.getVariableIndex("cPROJ"), mesh.projectionMatrix, true); +// applyShader(context3d, linkedProgram, mesh, camera); +// context3d.setVertexBufferAt(1, null); +// +// context3d.setCulling(Context3DTriangleFace.FRONT); +// context3d.drawTriangles(mesh.geometry.indexBuffer, 0, mesh.geometry.numTriangles); +// +// context3d.setVertexBufferAt(vLinker.getVariableIndex("aPOSITION"), null); +// context.setTextureAt(getTextureIndex(fLinker), texture); +// } + + } +} diff --git a/src/alternativa/engine3d/shadows/OmniShadowRendererDebugMaterial.as b/src/alternativa/engine3d/shadows/OmniShadowRendererDebugMaterial.as new file mode 100644 index 0000000..6dfd11a --- /dev/null +++ b/src/alternativa/engine3d/shadows/OmniShadowRendererDebugMaterial.as @@ -0,0 +1,162 @@ +package alternativa.engine3d.shadows { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.materials.*; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.resources.Geometry; + + import flash.display3D.Context3DProgramType; + import flash.display3D.textures.CubeTexture; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * @private + */ + public class OmniShadowRendererDebugMaterial extends Material { + + alternativa3d override function get canDrawInShadowMap():Boolean { + return false; + } + + static alternativa3d const _samplerSetProcedure:Procedure = new Procedure( + [ + "#v0=vUV", + "#s0=sTexture", + "#c0=cAlpha", + "tex t0, v0, s0 ", + "mov t0.w, c0.w", + "mov o0, t0" + ]); + + static alternativa3d const _samplerSetProcedureDiffuseAlpha:Procedure = new Procedure( + [ + "#v0=vUV", + "#s0=sTexture", + "#c0=cAlpha", + "tex t0, v0, s0 ", + "mul t0.w, t0.w, c0.w", + "mov o0, t0" + ]); + + static alternativa3d const _passUVProcedure:Procedure = new Procedure(["#v0=vUV", "#a0=aNORMAL", "mov v0, a0"]); + private static var _programs:Dictionary = new Dictionary(); + /** + * Текстура + */ + public var texture:CubeTexture; + /** + * Прозрачность + */ + public var alpha:Number = 1; + /** + * Использование alpha канала текстуры + */ + public var useDiffuseAlphaChannel:Boolean = false; + + /** + * Создает экземпляр материала + * @param texture текстура + * @param alpha прозрачность + */ + public function OmniShadowRendererDebugMaterial(texture:CubeTexture = null, alpha:Number = 1) { + this.texture = texture; + this.alpha = alpha; + } + + private function setupProgram(targetObject:Object3D):Vector. { + var optionsPrograms:Vector. = new Vector.(); + + var vertexLinker:Linker = new Linker(Context3DProgramType.VERTEX); + var positionVar:String = "aPosition"; + vertexLinker.declareVariable(positionVar, VariableType.ATTRIBUTE); + if (targetObject.transformProcedure != null) { + positionVar = appendPositionTransformProcedure(targetObject.transformProcedure, vertexLinker); + } + vertexLinker.addProcedure(_projectProcedure); + vertexLinker.setInputParams(_projectProcedure, positionVar); + vertexLinker.addProcedure(_passUVProcedure); + vertexLinker.link(); + + var fragmentLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + fragmentLinker.addProcedure(_samplerSetProcedure); + fragmentLinker.varyings = vertexLinker.varyings; + optionsPrograms[optionsPrograms.length] = new ShaderProgram(vertexLinker, fragmentLinker); + + var fragmentLinkerDiffuseAlpha:Linker = new Linker(Context3DProgramType.FRAGMENT); + fragmentLinkerDiffuseAlpha.addProcedure(_samplerSetProcedureDiffuseAlpha); + fragmentLinkerDiffuseAlpha.varyings = vertexLinker.varyings; + optionsPrograms[optionsPrograms.length] = new ShaderProgram(vertexLinker, fragmentLinkerDiffuseAlpha); + +// trace(A3DUtils.disassemble(fragmentLinker.getByteCode())); + + _programs[targetObject.transformProcedure] = optionsPrograms; + return optionsPrograms; + } + + /** + * @private + */ + override alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, objectRenderPriority:int = -1):void { + // TODO: repair + /* + destination.isValid = destination.isValid && texture != null; + + var optionsPrograms:Vector. = _programs[transformHolder.transformProcedure]; + if(!optionsPrograms) optionsPrograms = setupProgram(transformHolder); + var program:ShaderProgram; + if(!useDiffuseAlphaChannel){ + program = optionsPrograms[0]; + }else { + program = optionsPrograms[1]; + } + + if (!destination.isValid) { + return; + } + + if (alpha < 1 || useDiffuseAlphaChannel) { + destination.blendModeSource = Context3DBlendFactor.SOURCE_ALPHA; + destination.blendModeDestination = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA; + destination.renderPriority = Renderer.TRANSPARENT_SORT; + } else { + destination.blendModeSource = Context3DBlendFactor.ONE; + destination.blendModeDestination = Context3DBlendFactor.ZERO; + destination.renderPriority = Renderer.OPAQUE; + } + + destination.program = program; + geometry.setAttribute(destination, VertexAttributes.POSITION, program.vertexShader.getVariableIndex("aPosition")); + geometry.setAttribute(destination, VertexAttributes.NORMAL, program.vertexShader.getVariableIndex("aNORMAL")); + camera.composeProjectionMatrix(destination.constantSetVertexVectorValues, program.vertexShader.getVariableIndex("cProjMatrix") << 2, transformHolder.localToCameraTransform); + destination.constantSetVertexRegistersCount = destination.constantSetVertexVectorValues.length >> 2; + destination.addTextureSet(program.fragmentShader.getVariableIndex("sTexture"), texture); + destination.addFragmentConstantVector(program.fragmentShader.getVariableIndex("cAlpha"), 0, 0, 0, alpha); + destination.cullingMode = cullingMode; + */ + } + + /** + * @inheritDoc + */ + override public function clone():Material { + var res:OmniShadowRendererDebugMaterial = new OmniShadowRendererDebugMaterial(texture, alpha); + res.clonePropertiesFrom(this); + return res; + } + + override protected function clonePropertiesFrom(source:Material):void { + super.clonePropertiesFrom(source); + var t:OmniShadowRendererDebugMaterial = OmniShadowRendererDebugMaterial(source); + texture = t.texture; + alpha = t.alpha; + } + } +} diff --git a/src/alternativa/engine3d/shadows/Shadow.as b/src/alternativa/engine3d/shadows/Shadow.as new file mode 100644 index 0000000..080e510 --- /dev/null +++ b/src/alternativa/engine3d/shadows/Shadow.as @@ -0,0 +1,64 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.shadows { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Light3D; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.objects.Surface; + + use namespace alternativa3d; + + /** + * Base class for shadows. + */ + public class Shadow { + + /** + * @private + * Key for processing in materials. + */ + alternativa3d var type:String = "s"; + + /** + * @private + */ + alternativa3d var _light:Light3D; + + /** + * @private + * inputs: position + */ + alternativa3d var vertexShadowProcedure:Procedure; + + /** + * @private + * outputs: shadow intensity + */ + alternativa3d var fragmentShadowProcedure:Procedure; + + /** + * @private + */ + alternativa3d function process(camera:Camera3D):void { + } + + /** + * @private + */ + alternativa3d function setup(drawUnit:DrawUnit, vertexLinker:Linker, fragmentLinker:Linker, surface:Surface):void { + } + + } +} diff --git a/src/alternativa/engine3d/shadows/ShadowRenderer.as b/src/alternativa/engine3d/shadows/ShadowRenderer.as new file mode 100644 index 0000000..5c7b712 --- /dev/null +++ b/src/alternativa/engine3d/shadows/ShadowRenderer.as @@ -0,0 +1,184 @@ +package alternativa.engine3d.shadows { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.materials.ShaderProgram; + import alternativa.engine3d.materials.compiler.Procedure; + + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DCompareMode; + import flash.geom.Matrix3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class ShadowRenderer { + + alternativa3d var shaderKey:String; + private static var counter:int = 0; + + public var active:Boolean = false; + + public function ShadowRenderer() { + counter++; + shaderKey = "M" + counter.toString(); + } + + alternativa3d function get needMultiplyBlend():Boolean { + return false; + } + + public function update():void {} + + // Нет входных, выходных параметров + public function getVShader(index:int = 0):Procedure {return null} + public function getFShader(index:int = 0):Procedure {return null} + + public function getFIntensityShader():Procedure { throw new Error("Not implemented") }; + +// public function getMultVShader():Procedure {return null}; +// // i0 - input color +// // o0 - shadowed result +// public function getMultFShader():Procedure {return null}; + + public function applyShader(destination:DrawUnit, program:ShaderProgram, object:Object3D, camera:Camera3D, index:int = 0):void {} +// public function drawShadow(mesh:Mesh, camera:Camera3D, texture:Texture):void {} + +// public function getTextureIndex(fLinker:Linker):int {return 0}; + + public function get debug():Boolean { return false } + public function set debug(value:Boolean):void { } + + alternativa3d function cullReciever(boundBox:BoundBox, object:Object3D):Boolean { + return false; + } + + protected function cleanContext(context:Context3D):void { + context.setTextureAt(0, null); + context.setTextureAt(1, null); + context.setTextureAt(2, null); + context.setTextureAt(3, null); + context.setTextureAt(4, null); + context.setTextureAt(5, null); + context.setTextureAt(6, null); + context.setTextureAt(7, null); + context.setVertexBufferAt(1, null); + context.setVertexBufferAt(2, null); + context.setVertexBufferAt(3, null); + context.setVertexBufferAt(4, null); + context.setVertexBufferAt(5, null); + context.setVertexBufferAt(6, null); + context.setVertexBufferAt(7, null); + context.setDepthTest(true, Context3DCompareMode.LESS); + context.setBlendFactors(Context3DBlendFactor.ONE, Context3DBlendFactor.ZERO); + } + + static private const boundVertices:Vector. = new Vector.(24); + alternativa3d function cullObjectImplementation(bounds:BoundBox, matrix:Matrix3D):Boolean { + var i:int; + var infront:Boolean; + var behind:Boolean; + // Заполнение + boundVertices[0] = bounds.minX; + boundVertices[1] = bounds.minY; + boundVertices[2] = bounds.minZ; + boundVertices[3] = bounds.maxX; + boundVertices[4] = bounds.minY; + boundVertices[5] = bounds.minZ; + boundVertices[6] = bounds.minX; + boundVertices[7] = bounds.maxY; + boundVertices[8] = bounds.minZ; + boundVertices[9] = bounds.maxX; + boundVertices[10] = bounds.maxY; + boundVertices[11] = bounds.minZ; + boundVertices[12] = bounds.minX; + boundVertices[13] = bounds.minY; + boundVertices[14] = bounds.maxZ; + boundVertices[15] = bounds.maxX; + boundVertices[16] = bounds.minY; + boundVertices[17] = bounds.maxZ; + boundVertices[18] = bounds.minX; + boundVertices[19] = bounds.maxY; + boundVertices[20] = bounds.maxZ; + boundVertices[21] = bounds.maxX; + boundVertices[22] = bounds.maxY; + boundVertices[23] = bounds.maxZ; + + // Трансформация в камеру + matrix.transformVectors(boundVertices, boundVertices); + // Куллинг + for (i = 2, infront = false, behind = false; i <= 23; i += 3) { + if (boundVertices[i] > 0) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { + if (!infront) return false; + } + // left + for (i = 0, infront = false, behind = false; i <= 21; i += 3) { + if (boundVertices[i] > 0) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { + if (!infront) return false; + } + // right + for (i = 0, infront = false, behind = false; i <= 21; i += 3) { + if (boundVertices[i] < 1) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { + if (!infront) return false; + } + // up + for (i = 1, infront = false, behind = false; i <= 22; i += 3) { + if (boundVertices[i] > 0) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { + if (!infront) return false; + } + // down + for (i = 1, infront = false, behind = false; i <= 22; i += 3) { + if (boundVertices[i] < 1) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { + if (!infront) return false; + } + return true; + } + + } +} diff --git a/src/alternativa/engine3d/shadows/ShadowsSystem.as b/src/alternativa/engine3d/shadows/ShadowsSystem.as new file mode 100644 index 0000000..0481d04 --- /dev/null +++ b/src/alternativa/engine3d/shadows/ShadowsSystem.as @@ -0,0 +1,96 @@ +package alternativa.engine3d.shadows { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.Object3D; + + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * @private + */ + public class ShadowsSystem { + + private static const MAX_SHADOWMAPS:int = 3; +// private static const MAX_SHADOWMAPS:int = 4; + + public var renderers:Vector. = new Vector.(); + + private var containers:Dictionary = new Dictionary(); + + public function ShadowsSystem() { + } + + private var numShadowed:int; + + private var numActiveRenderers:int; + private var activeRenderers:Vector. = new Vector.(); + + private var maxShadows:int; + + public function update(root:Object3D):void { + if (renderers.length == 0) return; + numActiveRenderers = 0; + var num:int = renderers.length; + for (var i:int = 0; i < num; i++) { + var renderer:ShadowRenderer = renderers[i]; + renderer.update(); + if (renderer.active) { + activeRenderers[numActiveRenderers] = renderer; + numActiveRenderers++; + } + } + // Пробегаемся иерархически по объектам и проверяем наложение на них тени + if (root.transformChanged) root.composeTransforms(); + root.localToGlobalTransform.copy(root.transform); + numShadowed = 0; + maxShadows = 0; + recursive(root); +// trace("SHADOWED:", numShadowed, ":", maxShadows); + } + + private function recursive(object:Object3D):void { + for (var child:Object3D = object.childrenList; child != null; child = child.next) { + var value:Vector. = null; + var numRenderers:int = 0; + if (child.visible) { + if (child.transformChanged) child.composeTransforms(); + child.localToGlobalTransform.combine(object.localToGlobalTransform, child.transform); + for (var i:int = 0; i < numActiveRenderers; i++) { + var renderer:ShadowRenderer = activeRenderers[i]; + if (child.useShadow) { + if (child.boundBox == null || renderer.cullReciever(child.boundBox, child)) { + numShadowed++; + if (value == null) { + value = containers[child]; + if (value == null) { + value = new Vector.(); + containers[child] = value; + } else { + value.length = 0; + } + } + value[numRenderers] = renderer; + numRenderers++; + } + } + } + recursive(child); + } + setRenderers(child, value, numRenderers); + } + } + + private function setRenderers(object:Object3D, renderers:Vector., numShadowRenderers:int):void { + if (numShadowRenderers > maxShadows) maxShadows = numShadowRenderers; + if (numShadowRenderers > MAX_SHADOWMAPS) { + numShadowRenderers = MAX_SHADOWMAPS; + renderers.length = MAX_SHADOWMAPS; + } + object.shadowRenderers = renderers; + object.numShadowRenderers = numShadowRenderers; + } + + } +} diff --git a/src/alternativa/engine3d/shadows/SpotShadowRenderer.as b/src/alternativa/engine3d/shadows/SpotShadowRenderer.as new file mode 100644 index 0000000..965e5a1 --- /dev/null +++ b/src/alternativa/engine3d/shadows/SpotShadowRenderer.as @@ -0,0 +1,656 @@ +package alternativa.engine3d.shadows { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.core.VertexAttributes; + import alternativa.engine3d.lights.SpotLight; + import alternativa.engine3d.materials.ShaderProgram; + import alternativa.engine3d.materials.TextureMaterial; + import alternativa.engine3d.materials.compiler.Linker; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.objects.Surface; + import alternativa.engine3d.primitives.Box; + import alternativa.engine3d.resources.TextureResource; + + import flash.display3D.Context3D; + import flash.display3D.Context3DProgramType; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.Context3DTriangleFace; + import flash.display3D.Program3D; + import flash.display3D.textures.Texture; + import flash.geom.Matrix3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class SpotShadowRenderer extends ShadowRenderer { + + public var caster:Object3D; + + private var context:Context3D; + + private var shadowMap:Texture; + + private var light:SpotLight; + + private var debugObject:Mesh; + public var debugMaterial:TextureMaterial = new TextureMaterial(); + private var debugTexture:TextureResource = new TextureResource(); + + private var globalToShadowMap:Matrix3D = new Matrix3D(); + + private static const constants:Vector. = Vector.([ + 255, 255*0.96, 100, 1 + ]); + + private var pcfSize:Number = 0; + private var pcfOffset:Number = 0; + private var pcfOffsets:Vector.; + + public function SpotShadowRenderer(context:Context3D, size:int, pcfSize:Number = 0) { + this.context = context; + this.shadowMap = context.createTexture(size, size, Context3DTextureFormat.BGRA, true); + this.pcfSize = pcfSize; + debugTexture._texture = this.shadowMap; +// debugMaterial.diffuseMap = debugTexture; + debugMaterial.alpha = 0.9; + debugMaterial.opaquePass = false; + debugMaterial.transparentPass = true; + debugMaterial.alphaThreshold = 1.1; + } + + public function setLight(value:SpotLight):void { + light = value; + var width:Number = 2*Math.sin(light.falloff*0.5)*light.attenuationEnd; + this.pcfOffset = pcfSize/width/255; + if (pcfOffset > 0) { + pcfOffsets = Vector.([ + -pcfOffset, -pcfOffset, 0, 1/4, + -pcfOffset, pcfOffset, 0, 1, + pcfOffset, -pcfOffset, 0, 1, + pcfOffset, pcfOffset, 0, 1 + ]); + } + debugObject = new Box(width, width, 1, 1, 1, 1, false, debugMaterial); + debugObject.rotationX = Math.PI; + debugObject.geometry.upload(context); + if (_debug) { + light.addChild(debugObject); + } + } + + private var _debug:Boolean = false; + override public function get debug():Boolean { + return _debug; + } + + override public function set debug(value:Boolean):void { + _debug = value; + if (_debug) { + if (light != null) { + light.addChild(debugObject); + } + } else { + if (debugObject != null && debugObject._parent != null) { + debugObject._parent.removeChild(debugObject); + } + } + } + + private static var matrix:Matrix3D = new Matrix3D(); + override alternativa3d function cullReciever(boundBox:BoundBox, object:Object3D):Boolean { + copyMatrixFromTransform(matrix, object.localToGlobalTransform); + matrix.append(this.globalToShadowMap); + return cullObjectImpl(boundBox, matrix); + } + private function cullCaster(boundBox:BoundBox, objectToGlobal:Transform3D):Boolean { + copyMatrixFromTransform(matrix, objectToGlobal); + matrix.append(this.globalToShadowMap); + return cullObjectImpl(boundBox, matrix, light.attenuationEnd); + } + + private var projection:ProjectionTransform3D = new ProjectionTransform3D(); + private var uvProjection:Matrix3D = new Matrix3D(); + override public function update():void { + // Считаем матрицу перевода в лайт + var root:Object3D; + // Расчитываем матрицу объекта для перевода в глобал + // if (caster.transformChanged) { + caster.localToGlobalTransform.compose(caster._x, caster._y, caster._z, caster._rotationX, caster._rotationY, caster._rotationZ, caster._scaleX, caster._scaleY, caster._scaleZ); + // } else { + // caster.localToCameraTransform.copy(caster.transform); + // } + root = caster; + while (root._parent != null) { + root = root._parent; + // if (root.transformChanged) { + root.localToGlobalTransform.compose(root._x, root._y, root._z, root._rotationX, root._rotationY, root._rotationZ, root._scaleX, root._scaleY, root._scaleZ); + // } + caster.localToGlobalTransform.append(root.localToGlobalTransform); + } + + // Расчитываем матрицу лайта + light.localToGlobalTransform.compose(light._x, light._y, light._z, light._rotationX, light._rotationY, light._rotationZ, light._scaleX, light._scaleY, light._scaleZ); + root = light; + while (root._parent != null) { + root = root._parent; + // if (root.transformChanged) { + root.localToGlobalTransform.compose(root._x, root._y, root._z, root._rotationX, root._rotationY, root._rotationZ, root._scaleX, root._scaleY, root._scaleZ); + // } + light.localToGlobalTransform.append(root.localToGlobalTransform); + } + light.globalToLocalTransform.copy(light.localToGlobalTransform); + light.globalToLocalTransform.invert(); + + // Получаем матрицу перевода из объекта в лайт + caster.localToCameraTransform.combine(light.globalToLocalTransform, caster.localToGlobalTransform); +// caster.localToCameraTransform.append(light.globalToLocalTransform); + + // Считаем матрицу проецирования + calculateProjection(projection, uvProjection, light.falloff, 1, light.attenuationEnd); +// globalToShadowMap.copy(light.globalToLocalTransform); + copyMatrixFromTransform(globalToShadowMap, light.globalToLocalTransform); + globalToShadowMap.append(uvProjection); + + debugMaterial.diffuseMap = null; + +// trace("TEST:", testCasterCulling(caster)); + if (!testCasterCulling(caster)) { + active = false; + return; + } + active = true; + + // Рисуем в шедоумапу + context.setRenderToTexture(shadowMap, true, 0, 0); + // context.clear(1); + context.clear(1, 1, 1, 1); + cleanContext(context); + drawObjectToShadowMap(context, caster, projection); + context.setRenderToBackBuffer(); + cleanContext(context); + debugMaterial.diffuseMap = debugTexture; + } + + private function testCasterCulling(object:Object3D):Boolean { + for (var child:Object3D = object.childrenList; child != null; child = child.next) { + if (child.visible) { + if (child.transformChanged) child.composeTransforms(); + child.localToGlobalTransform.combine(object.localToGlobalTransform, child.transform); + if (child.boundBox == null || cullCaster(child.boundBox, child.localToGlobalTransform)) { + return true; + } + if (testCasterCulling(child)) { + return true; + } + } + } + return false; + } + + static private const boundVertices:Vector. = new Vector.(24); + alternativa3d function cullObjectImpl(bounds:BoundBox, matrix:Matrix3D, far:Number = 0):Boolean { + var i:int; + var infront:Boolean; + var behind:Boolean; + // Заполнение + boundVertices[0] = bounds.minX; + boundVertices[1] = bounds.minY; + boundVertices[2] = bounds.minZ; + boundVertices[3] = bounds.maxX; + boundVertices[4] = bounds.minY; + boundVertices[5] = bounds.minZ; + boundVertices[6] = bounds.minX; + boundVertices[7] = bounds.maxY; + boundVertices[8] = bounds.minZ; + boundVertices[9] = bounds.maxX; + boundVertices[10] = bounds.maxY; + boundVertices[11] = bounds.minZ; + boundVertices[12] = bounds.minX; + boundVertices[13] = bounds.minY; + boundVertices[14] = bounds.maxZ; + boundVertices[15] = bounds.maxX; + boundVertices[16] = bounds.minY; + boundVertices[17] = bounds.maxZ; + boundVertices[18] = bounds.minX; + boundVertices[19] = bounds.maxY; + boundVertices[20] = bounds.maxZ; + boundVertices[21] = bounds.maxX; + boundVertices[22] = bounds.maxY; + boundVertices[23] = bounds.maxZ; + + // Трансформация в камеру + matrix.transformVectors(boundVertices, boundVertices); + // Куллинг + // left + for (i = 0, infront = false, behind = false; i <= 21; i += 3) { +// trace("POS", boundVertices[i], boundVertices[int(i + 2)]); + if (boundVertices[i] > 0) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { +// trace("L", infront); + if (!infront) return false; + } + // right + for (i = 0, infront = false, behind = false; i <= 21; i += 3) { + if (boundVertices[i] < boundVertices[int(i + 2)]) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { +// trace("R", infront); + if (!infront) return false; + } + // up + for (i = 1, infront = false, behind = false; i <= 22; i += 3) { +// if (-boundVertices[i] < boundVertices[int(i + 1)]) { + if (boundVertices[i] > 0) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { +// trace("U", infront); + if (!infront) return false; + } + // down + for (i = 1, infront = false, behind = false; i <= 22; i += 3) { + if (boundVertices[i] < boundVertices[int(i + 1)]) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { +// trace("D", infront); + if (!infront) return false; + } + if (far > 0) { + for (i = 2, infront = false, behind = false; i <= 23; i += 3) { + if (boundVertices[i] < far) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { + // trace("N", infront); + if (!infront) return false; + } + } + for (i = 2, infront = false, behind = false; i <= 23; i += 3) { + if (boundVertices[i] > 0) { + infront = true; + if (behind) break; + } else { + behind = true; + if (infront) break; + } + } + if (behind) { + // trace("N", infront); + if (!infront) return false; + } + return true; + } + + // должен быть заполнен нулями + private var rawData:Vector. = new Vector.(16); + private var m:Matrix3D = new Matrix3D(); + private function calculateProjection(projection:ProjectionTransform3D, uvProjection:Matrix3D, fov:Number, nearClipping:Number, farClipping:Number):void { + var viewSize:Number = 1; + var focalLength:Number = viewSize/Math.tan(fov*0.5); + projection.m0 = focalLength/viewSize; + projection.m5 = -focalLength/viewSize; + projection.m10 = farClipping/(farClipping - nearClipping); + projection.m14 = -nearClipping*projection.m10; + + for (var i:int = 0; i < 16; i++) { + rawData[i] = 0; + } + + // TODO: предумножить матрицы + + rawData[0] = projection.m0; + rawData[5] = -projection.m5; + rawData[10]= projection.m10; + rawData[11]= 1; + rawData[14]= projection.m14; + uvProjection.rawData = rawData; + +// 0.5f, 0.0f, 0.0f, 0.0f, +// 0.0f, 0.5f, 0.0f, 0.0f, +// 0.0f, 0.0f, 0.5f, 0.0f, +// 0.5f, 0.5f, 0.5f, 1.0f + + rawData[0] = 0.5; + rawData[12] = 0.5; + rawData[5] = 0.5; + rawData[13] = 0.5; + rawData[10] = 0.5; + rawData[14] = 0.5; + rawData[11] = 0; + rawData[15] = 1; + m.rawData = rawData; + uvProjection.append(m); + } + + private var transformToMatrixRawData:Vector. = new Vector.(16); + private function copyMatrixFromTransform(matrix:Matrix3D, transform:Transform3D):void { + transformToMatrixRawData[0] = transform.a; + transformToMatrixRawData[1] = transform.e; + transformToMatrixRawData[2] = transform.i; + transformToMatrixRawData[3] = 0; + transformToMatrixRawData[4] = transform.b; + transformToMatrixRawData[5] = transform.f; + transformToMatrixRawData[6] = transform.j; + transformToMatrixRawData[7] = 0; + transformToMatrixRawData[8] = transform.c; + transformToMatrixRawData[9] = transform.g; + transformToMatrixRawData[10] = transform.k; + transformToMatrixRawData[11] = 0; + transformToMatrixRawData[12] = transform.d; + transformToMatrixRawData[13] = transform.h; + transformToMatrixRawData[14] = transform.l; + transformToMatrixRawData[15] = 1; + matrix.rawData = transformToMatrixRawData; + } + + alternativa3d static function drawObjectToShadowMap(context:Context3D, object:Object3D, projection:ProjectionTransform3D):void { + if (object is Mesh) { + drawMeshToShadowMap(context, Mesh(object), projection); + } + for (var child:Object3D = object.childrenList; child != null; child = child.next) { + if (child.visible) { + if (child.transformChanged) child.composeTransforms(); + child.localToCameraTransform.combine(object.localToCameraTransform, child.transform); + drawObjectToShadowMap(context, child, projection); + } + } + } + + private static var shadowMapProgram:Program3D; + private static var projectionVector:Vector. = new Vector.(16); + private static function drawMeshToShadowMap(context:Context3D, mesh:Mesh, projection:ProjectionTransform3D):void { + if (mesh.geometry == null || mesh.geometry.numTriangles == 0 || !mesh.geometry.isUploaded) { + return; + } + + if (shadowMapProgram == null) shadowMapProgram = initMeshToShadowMapProgram(context); + context.setProgram(shadowMapProgram); + + context.setVertexBufferAt(0, mesh.geometry.getVertexBuffer(VertexAttributes.POSITION), mesh.geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]); + + var transform:Transform3D = mesh.localToCameraTransform; + projectionVector[0] = transform.a*projection.m0; + projectionVector[1] = transform.b*projection.m0; + projectionVector[2] = transform.c*projection.m0; + projectionVector[3] = transform.d*projection.m0; + projectionVector[4] = transform.e*projection.m5; + projectionVector[5] = transform.f*projection.m5; + projectionVector[6] = transform.g*projection.m5; + projectionVector[7] = transform.h*projection.m5; + projectionVector[8] = transform.i*projection.m10; + projectionVector[9] = transform.j*projection.m10; + projectionVector[10] = transform.k*projection.m10; + projectionVector[11] = transform.l*projection.m10 + projection.m14; + projectionVector[12] = transform.i; + projectionVector[13] = transform.j; + projectionVector[14] = transform.k; + projectionVector[15] = transform.l; + + context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 0, projectionVector, 4); + context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 4, Vector.([255, 0, 0, 1])); + context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.([1/255, 0, 0, 1])); + + context.setCulling(Context3DTriangleFace.BACK); + for (var i:int = 0; i < mesh._surfacesLength; i++) { + var surface:Surface = mesh._surfaces[i]; + if (surface.material == null) continue; + context.drawTriangles(mesh.geometry._indexBuffer, surface.indexBegin, surface.numTriangles); + } + context.setVertexBufferAt(0, null); + } + + private static function initMeshToShadowMapProgram(context3d:Context3D):Program3D { + var vLinker:Linker = new Linker(Context3DProgramType.VERTEX); + var fLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); + var proc:Procedure = Procedure.compileFromArray([ + "#a0=a0", + "#c4=c4", + "#v0=v0", + "m44 t0, a0, c0", + "mul v0, t0, c4.x", + "mov o0, t0" + ]); + proc.assignVariableName(VariableType.CONSTANT, 0, "c0", 4); + vLinker.addProcedure(proc); + + fLinker.addProcedure(Procedure.compileFromArray([ + "#v0=v0", + "#c0=c0", + "mov t0.xy, v0.zz", + "frc t0.y, v0.z", + "sub t0.x, v0.z, t0.y", + "mul t0.x, t0.x, c0.x", + "mov t0.z, c0.z", + "mov t0.w, c0.w", + "mov o0, t0" + ])); + var program:Program3D = context3d.createProgram(); +// trace("VERTEX"); +// trace(A3DUtils.disassemble(vLinker.getByteCode())); +// trace("FRAGMENT"); +// trace(A3DUtils.disassemble(fLinker.getByteCode())); + fLinker.varyings = vLinker.varyings; + vLinker.link(); + fLinker.link(); + program.upload(vLinker.data, fLinker.data); + return program; + } + + // Rendering with shadow + private static function initVShader(index:int):Procedure { + var shader:Procedure = Procedure.compileFromArray([ + "m44 v0, a0, c0", + "mov v1, a0" + ]); + shader.assignVariableName(VariableType.ATTRIBUTE, 0, "aPosition"); + shader.assignVariableName(VariableType.CONSTANT, 0, index + "cTOSHADOW", 4); + shader.assignVariableName(VariableType.VARYING, 0, index + "vSHADOWSAMPLE"); + shader.assignVariableName(VariableType.VARYING, 1, "vPosition"); + return shader; + } + + private static function initFShader(mult:Boolean, usePCF:Boolean, index:int):Procedure { + var i:int; + var line:int = 0; + var shaderArr:Array = []; + var numPass:uint = (usePCF) ? 4 : 1; + for (i = 0; i < numPass; i++) { + // Расстояние + shaderArr[line++] = "mov t0.w, v0.z"; + shaderArr[line++] = "div t2, v0, v0.w"; + shaderArr[line++] = "mul t0.w, t0.w, c4.y"; // bias [0.99] * 255 + + if (usePCF) { + // Добавляем смещение + shaderArr[line++] = "mul t1, c" + (i + 9).toString() + ", t0.w"; +// shaderArr[line++] = "add t1, v0, t1"; + shaderArr[line++] = "add t1, t2, t1"; + shaderArr[line++] = "tex t1, t1, s0 <2d,clamp,near,nomip>"; + } else { +// shaderArr[line++] = "tex t1, v0, s0 <2d,clamp,near,nomip>"; + shaderArr[line++] = "tex t1, t2, s0 <2d,clamp,near,nomip>"; + } + + // Восстанавливаем расстояние + shaderArr[line++] = "mul t1.w, t1.x, c4.x"; // * 255 + shaderArr[line++] = "add t1.w, t1.w, t1.y"; + + // Перекрытие тенью + shaderArr[line++] = "sub t2.z, t1.w, t0.w"; + shaderArr[line++] = "mul t2.z, t2.z, c4.z"; // smooth [10000] + shaderArr[line++] = "sat t2.z, t2.z"; + + // Добавляем маску и прозрачность, затем sat + shaderArr[line++] = "add t2.z, t2.z, t1.z"; // маска тени + shaderArr[line++] = "add t2, t2.zzzz, c5"; // цвет тени + + // Плавный уход в прозрачность ------------- + shaderArr[line++] = "#c6=c" + index + "Spot"; + shaderArr[line++] = "#c7=c" + index + "Direction"; + shaderArr[line++] = "#c8=c" + index + "Geometry"; + shaderArr[line++] = "#v1=vPosition"; + // Считаем вектор из точки к свету + + // Вектор из точки к свету + shaderArr[line++] = "sub t0, c6, v1"; +// shaderArr[line++] = "sub t0, v1, c6"; + // Квадрат расстояния до точки + shaderArr[line++] = "dp3 t0.w, t0, t0"; + // Расстояние до точки + shaderArr[line++] = "sqt t0.w, t0.w"; + + // Нормализованное направление к источнику + shaderArr[line++] = "nrm t0.xyz, t0.xyz"; + // cos(Угол) между направлением к точке и направлением спота + shaderArr[line++] = "dp3 t0.y, t0.xyz, c7.xyz"; +// // cos(угол) - cos(falloff*0.5) + shaderArr[line++] = "sub t0.y, t0.y, c8.w"; +// // Делим на (cos(hotspot*0.5) - cos(falloff*0.5)) + shaderArr[line++] = "div t0.y, t0.y, c8.z"; + + // Минус atenuationBegin + shaderArr[line++] = "sub t0.w, t0.w, c8.y"; // len = len - atenuationBegin + // Делим на (atenuationEnd - atenuationBegin) + shaderArr[line++] = "div t0.w, t0.w, c8.x"; // att = len/radius + // 1 - соотношение между расстоянием до точки и максимальным расстоянием + shaderArr[line++] = "sub t0.w, c6.w, t0.w"; // att = 1 - len/radius + + shaderArr[line++] = "mul t0.w, t0.y, t0.w"; +// shaderArr[line++] = "mov t0.w, t0.y"; + shaderArr[line++] = "sub t0.w, c7.w, t0.w"; +// shaderArr[line++] = "mov t2, t0.wwww"; + shaderArr[line++] = "sat t0.w, t0.w"; + shaderArr[line++] = "add t2, t2, t0.wwww"; + // ----------------------------------------------------- + + shaderArr[line++] = "sat t2, t2"; + + if (usePCF) { + if (i == 0) { + shaderArr[line++] = "mov t3, t2"; + } else { + shaderArr[line++] = "add t3, t3, t2"; + } + } + } + if (usePCF) { + shaderArr[line++] = "mul t2, t3, c9.w"; + } + if (mult) { + shaderArr[line++] = "mul t0.xyz, i0.xyz, t2.xyz"; + shaderArr[line++] = "mov t0.w, i0.w"; + shaderArr[line++] = "mov o0, t0"; + } else { + shaderArr[line++] = "mov o0, t2"; +// shaderArr[line++] = "mov o0, t1"; + } + var shader:Procedure = Procedure.compileFromArray(shaderArr); + shader.assignVariableName(VariableType.VARYING, 0, index + "vSHADOWSAMPLE"); + shader.assignVariableName(VariableType.CONSTANT, 4, index + "cConstants", 1); + shader.assignVariableName(VariableType.CONSTANT, 5, index + "cShadowColor", 1); + if (usePCF) { + for (i = 0; i < numPass; i++) { + shader.assignVariableName(VariableType.CONSTANT, i + 9, "cSPCF" + i.toString(), 1); + } + } + shader.assignVariableName(VariableType.SAMPLER, 0, index + "sSHADOWMAP"); + return shader; + } + + override public function getFShader(index:int = 0):Procedure { + return initFShader(false, (pcfOffset > 0), index); + } + override public function getVShader(index:int = 0):Procedure { + return initVShader(index); + } + + private static const objectToShadowMap:Matrix3D = new Matrix3D(); + private static const localToGlobal:Transform3D = new Transform3D(); + override public function applyShader(drawUnit:DrawUnit, program:ShaderProgram, object:Object3D, camera:Camera3D, index:int = 0):void { + // Считаем матрицу перевода в лайт из объекта + localToGlobal.combine(camera.localToGlobalTransform, object.localToCameraTransform); + copyMatrixFromTransform(objectToShadowMap, localToGlobal); + objectToShadowMap.append(globalToShadowMap); + +// var casterPos:Vector3D = new Vector3D(caster.localToGlobalTransform.d, caster.localToGlobalTransform.h, caster.localToGlobalTransform.l); +// var p:Vector3D = objectToShadowMap.transformVector(casterPos); +// p.scaleBy(1/p.w); +// trace("caster pos:", p); + + objectToShadowMap.transpose(); + + drawUnit.setVertexConstantsFromVector(program.vertexShader.getVariableIndex(index + "cTOSHADOW"), objectToShadowMap.rawData, 4); + drawUnit.setFragmentConstantsFromVector(program.fragmentShader.getVariableIndex(index + "cConstants"), constants, constants.length/4); + drawUnit.setFragmentConstantsFromVector(program.fragmentShader.getVariableIndex(index + "cShadowColor"), camera.ambient, 1); + if (pcfOffset > 0) { + drawUnit.setFragmentConstantsFromVector(program.fragmentShader.getVariableIndex("cSPCF0"), pcfOffsets, pcfOffsets.length/4); + } + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex(index + "sSHADOWMAP"), shadowMap); + +// localToGlobal.combine(light.cameraToLocalTransform, object.localToCameraTransform); + localToGlobal.combine(object.cameraToLocalTransform, light.localToCameraTransform); +// localToGlobal.invert(); + + // Настройки затухания + var transform:Transform3D = localToGlobal; + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + index + "Spot"), transform.d, transform.h, transform.l, 1); + var rScale:Number = Math.sqrt(transform.a * transform.a + transform.e * transform.e + transform.i * transform.i); + rScale += Math.sqrt(transform.b * transform.b + transform.f * transform.f + transform.j * transform.j); + var dLen:Number = Math.sqrt(transform.c * transform.c + transform.g * transform.g + transform.k * transform.k); + rScale += dLen; + rScale /= 3; + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + index + "Direction"), -transform.c / dLen, -transform.g / dLen, -transform.k / dLen, 0.5); + + var falloff:Number = Math.cos(light.falloff * 0.5); + var hotspot:Number = Math.cos(light.hotspot * 0.5); + + drawUnit.setFragmentConstantsFromNumbers(program.fragmentShader.getVariableIndex("c" + index + "Geometry"), light.attenuationEnd * rScale - light.attenuationBegin * rScale, light.attenuationBegin * rScale, hotspot == falloff ? 0.000001 : hotspot - falloff, falloff); + } + + } +} + +class ProjectionTransform3D { + public var m0:Number; + public var m5:Number; + public var m10:Number; + public var m14:Number; +} diff --git a/src/alternativa/engine3d/shadows/StaticShadowRenderer.as b/src/alternativa/engine3d/shadows/StaticShadowRenderer.as new file mode 100644 index 0000000..18449ac --- /dev/null +++ b/src/alternativa/engine3d/shadows/StaticShadowRenderer.as @@ -0,0 +1,569 @@ +package alternativa.engine3d.shadows { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Camera3D; + import alternativa.engine3d.core.DrawUnit; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + import alternativa.engine3d.lights.DirectionalLight; + import alternativa.engine3d.materials.ShaderProgram; + import alternativa.engine3d.materials.TextureMaterial; + import alternativa.engine3d.materials.compiler.Procedure; + import alternativa.engine3d.materials.compiler.VariableType; + import alternativa.engine3d.objects.Mesh; + import alternativa.engine3d.primitives.Box; + import alternativa.engine3d.resources.ExternalTextureResource; + import alternativa.engine3d.resources.TextureResource; + + import flash.display3D.Context3D; + import flash.display3D.Context3DTextureFormat; + import flash.display3D.textures.Texture; + import flash.geom.Matrix3D; + import flash.geom.Vector3D; + import flash.utils.Dictionary; + + use namespace alternativa3d; + + /** + * @private + */ + public class StaticShadowRenderer extends ShadowRenderer { + + public var context:Context3D; + + private const alpha:Number = 0.7; + + private var bounds:BoundBox = new BoundBox(); + private var partSize:Number; + private var partsShadowMaps:Vector.> = new Vector.>(); + private var partsUVMatrices:Vector.> = new Vector.>(); + + private var light:DirectionalLight; + private var globalToLight:Transform3D = new Transform3D(); + + private var _debug:Boolean = false; + private var debugContainer:Object3D; + + private var _recievers:Dictionary = new Dictionary(); + public function addReciever(object:Object3D):void { + _recievers[object] = true; + } + public function removeReciever(object:Object3D):void { + delete _recievers[object]; + } + + private static const constants:Vector. = Vector.([ + // 255, 255*0.99, 100, 1/255 +// 255, 255*0.96, 100, 1 + 255, 255, 1000, 1 + ]); + + private var pcfOffset:Number = 0; + private static var pcfOffsets:Vector.; + + public function dispose():void { + for each (var textures:Vector. in partsShadowMaps) { + for each (var texture:Texture in textures) { + texture.dispose(); + } + } + partsShadowMaps.length = 0; + partsUVMatrices.length = 0; + } + + public function StaticShadowRenderer(context:Context3D, partSize:int, pcfSize:Number = 0) { + this.context = context; + this.partSize = partSize; + this.pcfOffset = pcfSize; + constants[3] = 1 - alpha; + } + + override alternativa3d function cullReciever(boundBox:BoundBox, object:Object3D):Boolean { + return _recievers[object]; + } + + private var lightProjectionMatrix:Matrix3D = new Matrix3D(); + public function calculateShadows(object:Object3D, light:DirectionalLight, widthPartsCount:int = 1, heightPartsCount:int = 1, overlap:Number = 0):void { + this.light = light; + + var root:Object3D; + // Расчитываем матрицу объекта +// if (object.transformChanged) { + object.localToCameraTransform.compose(object._x, object._y, object._z, object._rotationX, object._rotationY, object._rotationZ, object._scaleX, object._scaleY, object._scaleZ); +// } else { +// object.localToCameraTransform.copy(caster.transform); +// } + root = object; + while (root._parent != null) { + root = root._parent; +// if (root.transformChanged) { + root.localToGlobalTransform.compose(root._x, root._y, root._z, root._rotationX, root._rotationY, root._rotationZ, root._scaleX, root._scaleY, root._scaleZ); +// } + object.localToCameraTransform.append(root.localToGlobalTransform); + } + + // Расчитываем матрицу лайта + light.localToGlobalTransform.compose(light._x, light._y, light._z, light._rotationX, light._rotationY, light._rotationZ, light._scaleX, light._scaleY, light._scaleZ); + root = light; + while (root._parent != null) { + root = root._parent; +// if (root.transformChanged) { + root.localToGlobalTransform.compose(root._x, root._y, root._z, root._rotationX, root._rotationY, root._rotationZ, root._scaleX, root._scaleY, root._scaleZ); +// } + light.localToGlobalTransform.append(root.localToGlobalTransform); + } + light.globalToLocalTransform.copy(light.localToGlobalTransform); + light.globalToLocalTransform.invert(); + + globalToLight.copy(light.globalToLocalTransform); + + // Получаем матрицу перевода из объекта в лайт + object.localToCameraTransform.append(light.globalToLocalTransform); + + bounds.reset(); + calculateBoundBox(bounds, object); + + var frustumMinX:Number = bounds.minX; + var frustumMaxX:Number = bounds.maxX; + var frustumMinY:Number = bounds.minY; + var frustumMaxY:Number = bounds.maxY; + var frustumMinZ:Number = bounds.minZ; + var frustumMaxZ:Number = bounds.maxZ; + + // Считаем шаг + var halfOverlap:Number = overlap*0.5; + var partWorldWidth:Number = (frustumMaxX - frustumMinX)/widthPartsCount; + var partWorldHeight:Number = (frustumMaxY - frustumMinY)/heightPartsCount; + + debugContainer = new Object3D(); + if (_debug) { + light.addChild(debugContainer); + } + + // Создаем шэдоумапы и рендерим + for (var xIndex:int = 0; xIndex < widthPartsCount; xIndex++) { + var maps:Vector. = new Vector.(); + var matrices:Vector. = new Vector.(); + for (var yIndex:int = 0; yIndex < heightPartsCount; yIndex++) { + var leftX:Number = frustumMinX + xIndex*partWorldWidth; + var leftY:Number = frustumMinY + yIndex*partWorldHeight; + + var width:Number; + var height:Number; + if (xIndex == 0) { + width = partWorldWidth + halfOverlap; + } else if (xIndex == (widthPartsCount - 1)) { + leftX -= halfOverlap; + width = partWorldWidth + halfOverlap; + } else { + leftX -= halfOverlap; + width = partWorldWidth + overlap; + } + if (yIndex == 0) { + height = partWorldHeight + halfOverlap; + } else if (yIndex == (heightPartsCount - 1)) { + leftY -= halfOverlap; + height = partWorldHeight + halfOverlap; + } else { + leftY -= halfOverlap; + height = partWorldHeight + overlap; + } + + var uvMatrix:Matrix3D = new Matrix3D(); + calculateShadowMapProjection(lightProjectionMatrix, uvMatrix, leftX, leftY, frustumMinZ, leftX + width, leftY + height, frustumMaxZ); + + var shadowMap:Texture = context.createTexture(partSize, partSize, Context3DTextureFormat.BGRA, true); + // Рисуем в шедоумапу + context.setRenderToTexture(shadowMap, true, 0, 0); + context.clear(1, 1, 1, 0.5); + cleanContext(context); + DirectionalShadowRenderer.drawObjectToShadowMap(context, object, light, lightProjectionMatrix); + cleanContext(context); + + maps.push(shadowMap); + matrices.push(uvMatrix); + + var texture:TextureResource = new ExternalTextureResource(null); + texture._texture = shadowMap; + var material:TextureMaterial = new TextureMaterial(texture); + material.opaquePass = false; + material.transparentPass = true; + material.alphaThreshold = 1.1; + var debugObject:Mesh = new Box(width, height, 1, 1, 1, 1, false, material); +// var debugObject:Mesh = new Box(width, height, 1, 1, 1, 1, false, new FillMaterial()); + debugObject.geometry.upload(context); + debugObject.x = leftX + width/2; + debugObject.y = leftY + height/2; + debugObject.z = frustumMinZ; + debugContainer.addChild(debugObject); + } + partsShadowMaps.push(maps); + partsUVMatrices.push(matrices); + } + context.setRenderToBackBuffer(); + if (pcfOffset > 0) { + var offset:Number = pcfOffset/partWorldWidth; +// pcfOffsets = Vector.([ +// -offset, -offset, 0, 1/4, +// -offset, offset, 0, 1, +// offset, -offset, 0, 1, +// offset, offset, 0, 1, +// ]); + pcfOffsets = Vector.([ + -offset, -offset, 0, 1/4, + -offset, offset, 0, 1, + offset, -offset, 0, 1, + offset, offset, 0, 1 + ]); + } + } + + private static const points:Vector. = Vector.([ + new Vector3D(), new Vector3D(), new Vector3D(), new Vector3D(), + new Vector3D(), new Vector3D(), new Vector3D(), new Vector3D(), + new Vector3D(), new Vector3D(), new Vector3D(), new Vector3D(), + new Vector3D(), new Vector3D(), new Vector3D(), new Vector3D() + ]); + alternativa3d static function calculateBoundBox(boundBox:BoundBox, object:Object3D, hierarchy:Boolean = true):void { + // Считаем баунды объекта в лайте + var point:Vector3D; + if (object.boundBox != null) { + var bb:BoundBox = object.boundBox; + point = points[0]; + point.x = bb.minX; + point.y = bb.minY; + point.z = bb.minZ; + point = points[1]; + point.x = bb.minX; + point.y = bb.minY; + point.z = bb.maxZ; + point = points[2]; + point.x = bb.minX; + point.y = bb.maxY; + point.z = bb.minZ; + point = points[3]; + point.x = bb.minX; + point.y = bb.maxY; + point.z = bb.maxZ; + point = points[4]; + point.x = bb.maxX; + point.y = bb.minY; + point.z = bb.minZ; + point = points[5]; + point.x = bb.maxX; + point.y = bb.minY; + point.z = bb.maxZ; + point = points[6]; + point.x = bb.maxX; + point.y = bb.maxY; + point.z = bb.minZ; + point = points[7]; + point.x = bb.maxX; + point.y = bb.maxY; + point.z = bb.maxZ; + var transform:Transform3D = object.localToCameraTransform; + for (var i:int = 0; i < 8; i++) { + point = points[i]; + var x:Number = transform.a*point.x + transform.b*point.y + transform.c*point.z + transform.d; + var y:Number = transform.e*point.x + transform.f*point.y + transform.g*point.z + transform.h; + var z:Number = transform.i*point.x + transform.j*point.y + transform.k*point.z + transform.l; + if (x < boundBox.minX) { + boundBox.minX = x; + } + if (x > boundBox.maxX) { + boundBox.maxX = x; + } + if (y < boundBox.minY) { + boundBox.minY = y; + } + if (y > boundBox.maxY) { + boundBox.maxY = y; + } + if (z < boundBox.minZ) { + boundBox.minZ = z; + } + if (z > boundBox.maxZ) { + boundBox.maxZ = z; + } + } + } + if (hierarchy) { + // Пробегаемся по дочерним объектам + for (var child:Object3D = object.childrenList; child != null; child = child.next) { + if (child.visible) { + if (child.transformChanged) { + child.composeTransforms(); + } + child.localToCameraTransform.combine(object.localToCameraTransform, child.transform); + calculateBoundBox(boundBox, child); + } + } + } + } + + private var rawData:Vector. = new Vector.(16); + private function calculateShadowMapProjection(matrix:Matrix3D, uvMatrix:Matrix3D, frustumMinX:Number, frustumMinY:Number, frustumMinZ:Number, frustumMaxX:Number, frustumMaxY:Number, frustumMaxZ:Number):void { + // Считаем матрицу проецирования + rawData[0] = 2/(frustumMaxX - frustumMinX); + rawData[5] = 2/(frustumMaxY - frustumMinY); + rawData[10]= 1/(frustumMaxZ - frustumMinZ); + rawData[12] = (-0.5 * (frustumMaxX + frustumMinX) * rawData[0]); + rawData[13] = (-0.5 * (frustumMaxY + frustumMinY) * rawData[5]); + rawData[14]= -frustumMinZ/(frustumMaxZ - frustumMinZ); + rawData[15]= 1; + matrix.rawData = rawData; + + rawData[0] = 1/((frustumMaxX - frustumMinX)); + // if (useSingle) { + // rawData[5] = 1/((frustumMaxY - frustumMinY)); + // } else { + rawData[5] = -1/((frustumMaxY - frustumMinY)); + // } + rawData[12] = 0.5 - (0.5 * (frustumMaxX + frustumMinX) * rawData[0]); + rawData[13] = 0.5 - (0.5 * (frustumMaxY + frustumMinY) * rawData[5]); + uvMatrix.rawData = rawData; + } + + override public function get debug():Boolean { + return _debug; + } + + override public function set debug(value:Boolean):void { + _debug = value; + if (debugContainer != null) { + if (value) { + if (light != null) { + light.addChild(debugContainer); + } + } else { + if (debugContainer._parent != null) { + debugContainer.removeFromParent(); + } + } + } + } + +// private static const vShader:Procedure = initVShader(index); + private static function initVShader(index:int):Procedure { + var shader:Procedure = Procedure.compileFromArray([ +// "m44 o0, a0, c0", +// Координата вершины в локальном пространстве + "m44 v0, a0, c4" + ]); + shader.assignVariableName(VariableType.ATTRIBUTE, 0, "aPosition"); + shader.assignVariableName(VariableType.CONSTANT, 0, "cPROJ", 4); + shader.assignVariableName(VariableType.CONSTANT, 4, "cTOSHADOW", 4); + shader.assignVariableName(VariableType.VARYING, 0, "vSHADOWSAMPLE"); + return shader; + } +// private static const multVShader:Procedure = initMultVShader(); +// private static function initMultVShader():Procedure { +// var shader:Procedure = Procedure.compileFromArray([ +// "m44 v0, a0, c0", +// ]); +// shader.assignVariableName(VariableType.ATTRIBUTE, 0, "aPOSITION"); +// shader.assignVariableName(VariableType.CONSTANT, 0, "cTOSHADOW", 4); +// shader.assignVariableName(VariableType.VARYING, 0, "vSHADOWSAMPLE"); +// return shader; +// } +// private static const fShader:Procedure = initFShader(false, false, index); +// private static const pcfFShader:Procedure = initFShader(false, true, index); + // i0 - input color + // o0 - shadowed result +// private static const multFShader:Procedure = initFShader(true, false, index); +// private static const pcfMultFShader:Procedure = initFShader(true, true, index); + private static function initFShader(mult:Boolean, usePCF:Boolean, index:int, grayScale:Boolean = false):Procedure { + var i:int; + var line:int = 0; + var shaderArr:Array = []; + var numPass:uint = (usePCF) ? 4 : 1; + for (i = 0; i < numPass; i++) { + // Расстояние + shaderArr[line++] = "mov t0.w, v0.z"; + shaderArr[line++] = "mul t0.w, t0.w, c4.y"; // bias [0.99] * 255 + + if (usePCF) { + // Добавляем смещение +// shaderArr[line++] = "mul t1, c" + (i + 5).toString() + ", t0.w"; + shaderArr[line++] = "add t1, v0, c" + (i + 5).toString() + ""; + // shaderArr[line++] = "add t1, v0, c" + (i + 5).toString(); + shaderArr[line++] = "tex t1, t1, s0 <2d,clamp,near,nomip>"; + } else { + shaderArr[line++] = "tex t1, v0, s0 <2d,clamp,near,nomip>"; + } + + // Восстанавливаем расстояние + shaderArr[line++] = "mul t1.w, t1.x, c4.x"; // * 255 + shaderArr[line++] = "add t1.w, t1.w, t1.y"; + + // Перекрытие тенью + shaderArr[line++] = "sub t2.z, t1.w, t0.w"; + shaderArr[line++] = "mul t2.z, t2.z, c4.z"; // smooth [10000] + shaderArr[line++] = "sat t2.z, t2.z"; + + // Добавляем маску и прозрачность, затем sat +// shaderArr[line++] = "add t2.z, t2.z, t1.z"; // маска тени +// shaderArr[line++] = "add t2.z, t2.z, c4.w"; // вес тени + shaderArr[line++] = "sat t2.z, t2.z"; + + if (usePCF) { + if (i == 0) { + shaderArr[line++] = "mov t2.x, t2.z"; + } else { + shaderArr[line++] = "add t2.x, t2.x, t2.z"; + } + } + } + if (usePCF) { + shaderArr[line++] = "mul t2.z, t2.x, c5.w"; + } + if (grayScale) { + shaderArr.push("mov o0.w, t2.z"); + } else { + if (mult) { + shaderArr.push("mul t0.xyz, i0.xyz, t2.z"); + shaderArr.push("mov t0.w, i0.w"); + shaderArr.push("mov o0, t0"); + } else { + shaderArr.push("mov t0, t2.z"); + shaderArr.push("mov o0, t0"); + } + } + var shader:Procedure = Procedure.compileFromArray(shaderArr, "StaticShadowMap"); + shader.assignVariableName(VariableType.VARYING, 0, "vSHADOWSAMPLE"); + shader.assignVariableName(VariableType.CONSTANT, 4, "cConstants", 1); + if (usePCF) { + for (i = 0; i < numPass; i++) { + shader.assignVariableName(VariableType.CONSTANT, i + 5, "cPCF" + i.toString(), 1); + } + } + shader.assignVariableName(VariableType.SAMPLER, 0, "sSHADOWMAP"); + return shader; + } + + override public function getVShader(index:int = 0):Procedure { + return initVShader(index); + } + override public function getFShader(index:int = 0):Procedure { + return initFShader(false, (pcfOffset > 0), index); + } + +// override public function getMultFShader():Procedure { +// return (pcfOffset > 0) ? pcfMultFShader : multFShader; +// } +// override public function getMultVShader():Procedure { +// return multVShader; +// } + + override public function getFIntensityShader():Procedure { + return initFShader(false, (pcfOffset > 0), 0, true); + } + + private static const objectToShadowMap:Transform3D = new Transform3D(); + private static const objectToUVMap:Matrix3D = new Matrix3D(); + override public function applyShader(drawUnit:DrawUnit, program:ShaderProgram, object:Object3D, camera:Camera3D, index:int = 0):void { + // Считаем матрицу перевода в лайт из объекта + + objectToShadowMap.combine(camera.localToGlobalTransform, object.localToCameraTransform); + objectToShadowMap.append(globalToLight); + +// objectToShadowMap.identity(); +// objectToShadowMap.append(object.cameraMatrix); +// objectToShadowMap.append(camera.globalMatrix); +// objectToShadowMap.append(globalToLight); + + // Получаем индекс шедоумапы +// var coords:Vector3D = objectToShadowMap.position; + var coords:Vector3D = new Vector3D(objectToShadowMap.d, objectToShadowMap.h, objectToShadowMap.l); + var xIndex:int = (coords.x - bounds.minX)/(bounds.maxX - bounds.minX)*partsShadowMaps.length; + + xIndex = (xIndex < 0) ? 0 : ((xIndex >= partsShadowMaps.length) ? partsShadowMaps.length - 1 : xIndex); + var maps:Vector. = partsShadowMaps[xIndex]; + var matrices:Vector. = partsUVMatrices[xIndex]; + + var yIndex:int = (coords.y - bounds.minY)/(bounds.maxY - bounds.minY)*maps.length; + yIndex = (yIndex < 0) ? 0 : ((yIndex >= maps.length) ? maps.length - 1 : yIndex); + +// trace(xIndex, yIndex); + + var shadowMap:Texture = maps[yIndex]; + var uvMatrix:Matrix3D = matrices[yIndex]; + + DirectionalShadowRenderer.copyMatrixFromTransform(objectToUVMap, objectToShadowMap); + objectToUVMap.append(uvMatrix); + objectToUVMap.transpose(); +// objectToShadowMap.append(uvMatrix); + + drawUnit.setVertexConstantsFromVector(program.vertexShader.getVariableIndex("cTOSHADOW"), objectToUVMap.rawData, 4); + drawUnit.setFragmentConstantsFromVector(program.fragmentShader.getVariableIndex("cConstants"), constants, 1); + if (pcfOffset > 0) { + drawUnit.setFragmentConstantsFromVector(program.fragmentShader.getVariableIndex("cPCF0"), pcfOffsets, pcfOffsets.length >> 2) + } + drawUnit.setTextureAt(program.fragmentShader.getVariableIndex("sSHADOWMAP"), shadowMap); + } + +// private static var program:LinkedProgram; +// private static var programPCF:LinkedProgram; +// private static function initMeshProgram(context:Context3D, usePCF:Boolean):LinkedProgram { +// var vLinker:Linker = new Linker(Context3DProgramType.VERTEX); +// vLinker.addShader(vShader); +// +// var fLinker:Linker = new Linker(Context3DProgramType.FRAGMENT); +// if (usePCF) { +// fLinker.addShader(pcfFShader); +// } else { +// fLinker.addShader(fShader); +// } +// +// vLinker.setOppositeLinker(fLinker); +// fLinker.setOppositeLinker(vLinker); +// +//// trace("[VERTEX]"); +//// trace(AgalUtils.disassemble(vLinker.getByteCode())); +//// trace("[FRAGMENT]"); +//// trace(AgalUtils.disassemble(fLinker.getByteCode())); +// +// var result:LinkedProgram; +// if (usePCF) { +// programPCF = new LinkedProgram(); +// result = programPCF; +// } else { +// program = new LinkedProgram(); +// result = program; +// } +// result.vLinker = vLinker; +// result.fLinker = fLinker; +// result.program = context.createProgram(); +// result.program.upload(vLinker.getByteCode(), fLinker.getByteCode()); +// +// return result; +// } +// +// override public function drawShadow(mesh:Mesh, camera:Camera3D, texture:Texture):void { +// var context3d:Context3D = camera.view._context3d; +// +// var linkedProgram:LinkedProgram; +// if (pcfOffset > 0) { +// linkedProgram = (programPCF == null) ? initMeshProgram(context3d, true) : programPCF; +// } else { +// linkedProgram = (program == null) ? initMeshProgram(context3d, false) : program; +// } +// var vLinker:Linker = linkedProgram.vLinker; +// var fLinker:Linker = linkedProgram.fLinker; +// context3d.setProgram(linkedProgram.program); +// +// context3d.setVertexBufferAt(vLinker.getVariableIndex("aPOSITION"), mesh.geometry.vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3); +// context3d.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, vLinker.getVariableIndex("cPROJ"), mesh.projectionMatrix, true); +// applyShader(context3d, linkedProgram, mesh, camera); +// context3d.setVertexBufferAt(1, null); +// +// context3d.setCulling(Context3DTriangleFace.FRONT); +// context3d.drawTriangles(mesh.geometry.indexBuffer, 0, mesh.geometry.numTriangles); +// +// context3d.setVertexBufferAt(vLinker.getVariableIndex("aPOSITION"), null); +// context.setTextureAt(getTextureIndex(fLinker), texture); +// } + + } +} diff --git a/src/alternativa/engine3d/utils/Object3DUtils.as b/src/alternativa/engine3d/utils/Object3DUtils.as new file mode 100644 index 0000000..ba55984 --- /dev/null +++ b/src/alternativa/engine3d/utils/Object3DUtils.as @@ -0,0 +1,92 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.engine3d.utils { + + import alternativa.engine3d.alternativa3d; + import alternativa.engine3d.core.BoundBox; + import alternativa.engine3d.core.Object3D; + import alternativa.engine3d.core.Transform3D; + + use namespace alternativa3d; + + /** + * @private + */ + public class Object3DUtils { + + private static const toRootTransform:Transform3D = new Transform3D(); + private static const fromRootTransform:Transform3D = new Transform3D(); + + /** + * @private + * Performs calculation of bound box of objects hierarchy branch. + */ + public static function calculateHierarchyBoundBox(object:Object3D, boundBoxSpace:Object3D = null, result:BoundBox = null):BoundBox { + if (result == null) result = new BoundBox(); + + if (boundBoxSpace != null && object != boundBoxSpace) { + // Calculate transfer matrix from object to provided space. + var objectRoot:Object3D; + var toSpaceTransform:Transform3D = null; + + if (object.transformChanged) object.composeTransforms(); + toRootTransform.copy(object.transform); + var root:Object3D = object; + while (root._parent != null) { + root = root._parent; + if (root.transformChanged) root.composeTransforms(); + toRootTransform.append(root.transform); + if (root == boundBoxSpace) { + // Matrix has been composed. + toSpaceTransform = toRootTransform; + } + } + objectRoot = root; + if (toSpaceTransform == null) { + // Transfer matrix from root to needed space. + if (boundBoxSpace.transformChanged) boundBoxSpace.composeTransforms(); + fromRootTransform.copy(boundBoxSpace.inverseTransform); + root = boundBoxSpace; + while (root._parent != null) { + root = root._parent; + if (root.transformChanged) root.composeTransforms(); + fromRootTransform.prepend(root.inverseTransform); + } + if (objectRoot == root) { + toRootTransform.append(fromRootTransform); + toSpaceTransform = toRootTransform; + } else { + throw new ArgumentError("Object and boundBoxSpace must be located in the same hierarchy."); + } + } + updateBoundBoxHierarchically(object, result, toSpaceTransform); + } else { + updateBoundBoxHierarchically(object, result); + } + return result; + } + + /** + * @private + * Calculates hierarchical bound. + */ + alternativa3d static function updateBoundBoxHierarchically(object:Object3D, boundBox:BoundBox, transform:Transform3D = null):void { + object.updateBoundBox(boundBox, transform); + for (var child:Object3D = object.childrenList; child != null; child = child.next) { + if (child.transformChanged) child.composeTransforms(); + child.localToCameraTransform.copy(child.transform); + if (transform != null) child.localToCameraTransform.append(transform); + updateBoundBoxHierarchically(child, boundBox, child.localToCameraTransform); + } + } + + } +} diff --git a/src/alternativa/osgi/service/clientlog/IClientLog.as b/src/alternativa/osgi/service/clientlog/IClientLog.as new file mode 100644 index 0000000..d6691df --- /dev/null +++ b/src/alternativa/osgi/service/clientlog/IClientLog.as @@ -0,0 +1,73 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.osgi.service.clientlog { + + /** + * @private + */ + public interface IClientLog extends IClientLogBase { + + /** + * Adds record in specified log, duplicating its into the "error" channel. + * + * @param channelName Name of channel. + * @param text Text, that can contain expressions like %i, i=1..n. These expressions will be changed by values, that is passed as subsequent parameters. + * @param vars Values of variables in text. + */ + function logError(channelName:String, text:String, ...vars):void; + + /** + * Returns list of strings at specified channel. If channel is not exists, null is returned. + * + * @param channelName Name of channel + * @return List of string in specified channel. + */ + function getChannelStrings(channelName:String):Vector.; + + /** + * Add listener for all channel of log. + * + * @param listener Listener. + */ + function addLogListener(listener:IClientLogChannelListener):void; + + /** + * Removes listener from all channels of log. + * + * @param listener Listener. + */ + function removeLogListener(listener:IClientLogChannelListener):void; + + /** + * Add listener of channel. + * + * @param channelName Name of channel, for which the listener is added. + * @param listener Listener. + */ + function addLogChannelListener(channelName:String, listener:IClientLogChannelListener):void; + + /** + * Removes listener of channel. + * + * @param channelName Name of channel, for which the listener is removed. + * @param listener Listener. + */ + function removeLogChannelListener(channelName:String, listener:IClientLogChannelListener):void; + + /** + * Returns list of existing channels. + * + * @return List of existing channels. + */ + function getChannelNames():Vector.; + + } +} diff --git a/src/alternativa/osgi/service/clientlog/IClientLogChannelListener.as b/src/alternativa/osgi/service/clientlog/IClientLogChannelListener.as new file mode 100644 index 0000000..a257fe9 --- /dev/null +++ b/src/alternativa/osgi/service/clientlog/IClientLogChannelListener.as @@ -0,0 +1,21 @@ +/** + * Exhibit A - Source Code Form License Notice + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + * If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + * You may add additional accurate notices of copyright ownership. + * + * It is desirable to notify that Covered Software was "Powered by AlternativaPlatform" with link to http://www.alternativaplatform.com/ + * */ + +package alternativa.osgi.service.clientlog { + + /** + * @private + */ + public interface IClientLogChannelListener { + + function onLogEntryAdded(channelName:String, logText:String):void; + + } +}