From 91477b73f5be6e6a876ff45db865c329d9e140df Mon Sep 17 00:00:00 2001 From: "rem..om" Date: Fri, 12 Aug 2011 05:50:31 +0000 Subject: [PATCH] Engine : - Added Volume texture 3D loading support to the DDSLoader - Added a test case for texture 3D loading - fixes a minor log issue in Material.java git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@8003 75d07b2b-3a1a-0410-a2c5-0572b91ccdca --- .../com/jme3/texture/plugins/DDSLoader.java | 256 ++++++++++++++---- .../src/core/com/jme3/asset/TextureKey.java | 49 ++-- .../src/core/com/jme3/material/Material.java | 7 +- engine/src/test-data/Textures/3D/flame.dds | Bin 0 -> 131200 bytes .../texture/TestTexture3DLoading.java | 65 +++++ .../src/test/jme3test/texture/tex3DThumb.frag | 14 + .../src/test/jme3test/texture/tex3DThumb.j3md | 18 ++ .../src/test/jme3test/texture/tex3DThumb.vert | 11 + 8 files changed, 345 insertions(+), 75 deletions(-) create mode 100644 engine/src/test-data/Textures/3D/flame.dds create mode 100644 engine/src/test/jme3test/texture/TestTexture3DLoading.java create mode 100644 engine/src/test/jme3test/texture/tex3DThumb.frag create mode 100644 engine/src/test/jme3test/texture/tex3DThumb.j3md create mode 100644 engine/src/test/jme3test/texture/tex3DThumb.vert diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java b/engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java index 81fb858fc..d89767dc9 100644 --- a/engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java +++ b/engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java @@ -29,7 +29,6 @@ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - package com.jme3.texture.plugins; import com.jme3.asset.AssetInfo; @@ -62,14 +61,11 @@ public class DDSLoader implements AssetLoader { private static final Logger logger = Logger.getLogger(DDSLoader.class.getName()); private static final boolean forceRGBA = false; - private static final int DDSD_MANDATORY = 0x1007; private static final int DDSD_MANDATORY_DX10 = 0x6; - private static final int DDSD_MIPMAPCOUNT = 0x20000; private static final int DDSD_LINEARSIZE = 0x80000; private static final int DDSD_DEPTH = 0x800000; - private static final int DDPF_ALPHAPIXELS = 0x1; private static final int DDPF_FOURCC = 0x4; private static final int DDPF_RGB = 0x40; @@ -79,31 +75,24 @@ public class DDSLoader implements AssetLoader { private static final int DDPF_ALPHA = 0x2; // used by NVTextureTools to mark normal images. private static final int DDPF_NORMAL = 0x80000000; - private static final int SWIZZLE_xGxR = 0x78477852; - private static final int DDSCAPS_COMPLEX = 0x8; private static final int DDSCAPS_TEXTURE = 0x1000; private static final int DDSCAPS_MIPMAP = 0x400000; - private static final int DDSCAPS2_CUBEMAP = 0x200; private static final int DDSCAPS2_VOLUME = 0x200000; - private static final int PF_DXT1 = 0x31545844; private static final int PF_DXT3 = 0x33545844; private static final int PF_DXT5 = 0x35545844; private static final int PF_ATI1 = 0x31495441; private static final int PF_ATI2 = 0x32495441; // 0x41544932; private static final int PF_DX10 = 0x30315844; // a DX10 format - private static final int DX10DIM_BUFFER = 0x1, - DX10DIM_TEXTURE1D = 0x2, - DX10DIM_TEXTURE2D = 0x3, - DX10DIM_TEXTURE3D = 0x4; - + DX10DIM_TEXTURE1D = 0x2, + DX10DIM_TEXTURE2D = 0x3, + DX10DIM_TEXTURE3D = 0x4; private static final int DX10MISC_GENERATE_MIPS = 0x1, - DX10MISC_TEXTURECUBE = 0x4; - + DX10MISC_TEXTURECUBE = 0x4; private static final double LOG2 = Math.log(2); private int width; private int height; @@ -115,60 +104,62 @@ public class DDSLoader implements AssetLoader { private int caps2; private boolean directx10; private boolean compressed; + private boolean texture3D; private boolean grayscaleOrAlpha; private boolean normal; private Format pixelFormat; private int bpp; private int[] sizes; - private int redMask, greenMask, blueMask, alphaMask; + private int redMask, greenMask, blueMask, alphaMask; private DataInput in; public DDSLoader() { } - public Object load(AssetInfo info) throws IOException{ - if (!(info.getKey() instanceof TextureKey)) + public Object load(AssetInfo info) throws IOException { + if (!(info.getKey() instanceof TextureKey)) { throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey"); + } InputStream stream = info.openStream(); in = new LittleEndien(stream); loadHeader(); - ArrayList data = readData( ((TextureKey)info.getKey()).isFlipY() ); - stream.close(); - return new Image(pixelFormat, width, height, 0, data, sizes); + ArrayList data = readData(((TextureKey) info.getKey()).isFlipY()); + stream.close(); + return new Image(pixelFormat, width, height, depth, data, sizes); } - public Image load(InputStream stream) throws IOException{ + public Image load(InputStream stream) throws IOException { in = new LittleEndien(stream); loadHeader(); ArrayList data = readData(false); - return new Image(pixelFormat, width, height, 0, data, sizes); + return new Image(pixelFormat, width, height, depth, data, sizes); } - private void loadDX10Header() throws IOException{ + private void loadDX10Header() throws IOException { int dxgiFormat = in.readInt(); - if (dxgiFormat != 83){ - throw new IOException("Only DXGI_FORMAT_BC5_UNORM " + - "is supported for DirectX10 DDS! Got: "+dxgiFormat); + if (dxgiFormat != 83) { + throw new IOException("Only DXGI_FORMAT_BC5_UNORM " + + "is supported for DirectX10 DDS! Got: " + dxgiFormat); } pixelFormat = Format.LATC; bpp = 8; compressed = true; int resDim = in.readInt(); - if (resDim == DX10DIM_TEXTURE3D){ - // mark texture as 3D + if (resDim == DX10DIM_TEXTURE3D) { + texture3D = true; } int miscFlag = in.readInt(); int arraySize = in.readInt(); - if (is(miscFlag, DX10MISC_TEXTURECUBE)){ + if (is(miscFlag, DX10MISC_TEXTURECUBE)) { // mark texture as cube - if (arraySize != 6){ + if (arraySize != 6) { throw new IOException("Cubemaps should consist of 6 images!"); } } - + in.skipBytes(4); // skip reserved value } @@ -185,7 +176,7 @@ public class DDSLoader implements AssetLoader { if (!is(flags, DDSD_MANDATORY) && !is(flags, DDSD_MANDATORY_DX10)) { throw new IOException("Mandatory flags missing"); } - + height = in.readInt(); width = in.readInt(); pitchOrSize = in.readInt(); @@ -199,17 +190,23 @@ public class DDSLoader implements AssetLoader { caps2 = in.readInt(); in.skipBytes(12); - if (!directx10){ + + if (!directx10) { if (!is(caps1, DDSCAPS_TEXTURE)) { throw new IOException("File is not a texture"); } - if (depth <= 0) + if (depth <= 0) { depth = 1; + } if (is(caps2, DDSCAPS2_CUBEMAP)) { depth = 6; // somewhat of a hack, force loading 6 textures if a cubemap } + + if (is(caps2, DDSCAPS2_VOLUME)) { + texture3D = true; + } } int expectedMipmaps = 1 + (int) Math.ceil(Math.log(Math.max(height, width)) / LOG2); @@ -220,14 +217,14 @@ public class DDSLoader implements AssetLoader { } else if (mipMapCount != expectedMipmaps) { // changed to warning- images often do not have the required amount, // or specify that they have mipmaps but include only the top level.. - logger.log(Level.WARNING, "Got {0} mipmaps, expected {1}", + logger.log(Level.WARNING, "Got {0} mipmaps, expected {1}", new Object[]{mipMapCount, expectedMipmaps}); } } else { mipMapCount = 1; } - if (directx10){ + if (directx10) { loadDX10Header(); } @@ -268,7 +265,7 @@ public class DDSLoader implements AssetLoader { case PF_DXT5: bpp = 8; pixelFormat = Image.Format.DXT5; - if (swizzle == SWIZZLE_xGxR){ + if (swizzle == SWIZZLE_xGxR) { normal = true; } break; @@ -297,7 +294,7 @@ public class DDSLoader implements AssetLoader { logger.warning("Must use linear size with fourcc"); pitchOrSize = size; } else if (pitchOrSize != size) { - logger.log(Level.WARNING, "Expected size = {0}, real = {1}", + logger.log(Level.WARNING, "Expected size = {0}, real = {1}", new Object[]{size, pitchOrSize}); } } else { @@ -321,7 +318,7 @@ public class DDSLoader implements AssetLoader { } else { pixelFormat = Format.RGB8; } - } else if (is(pfFlags, DDPF_GRAYSCALE) && is(pfFlags, DDPF_ALPHAPIXELS)){ + } else if (is(pfFlags, DDPF_GRAYSCALE) && is(pfFlags, DDPF_ALPHAPIXELS)) { switch (bpp) { case 16: pixelFormat = Format.Luminance8Alpha8; @@ -368,7 +365,7 @@ public class DDSLoader implements AssetLoader { logger.warning("Linear size said to contain valid value but does not"); pitchOrSize = size; } else if (pitchOrSize != size) { - logger.log(Level.WARNING, "Expected size = {0}, real = {1}", + logger.log(Level.WARNING, "Expected size = {0}, real = {1}", new Object[]{size, pitchOrSize}); } } else { @@ -464,9 +461,9 @@ public class DDSLoader implements AssetLoader { */ public ByteBuffer readRGB2D(boolean flip, int totalSize) throws IOException { int redCount = count(redMask), - blueCount = count(blueMask), - greenCount = count(greenMask), - alphaCount = count(alphaMask); + blueCount = count(blueMask), + greenCount = count(greenMask), + alphaCount = count(alphaMask); if (redMask == 0x00FF0000 && greenMask == 0x0000FF00 && blueMask == 0x000000FF) { if (alphaMask == 0xFF000000 && bpp == 32) { @@ -536,23 +533,20 @@ public class DDSLoader implements AssetLoader { int mipWidth = width; int mipHeight = height; - int offset = 0; for (int mip = 0; mip < mipMapCount; mip++) { - if (flip){ + if (flip) { byte[] data = new byte[sizes[mip]]; in.readFully(data); ByteBuffer wrapped = ByteBuffer.wrap(data); wrapped.rewind(); ByteBuffer flipped = DXTFlipper.flipDXT(wrapped, mipWidth, mipHeight, pixelFormat); buffer.put(flipped); - }else{ + } else { byte[] data = new byte[sizes[mip]]; in.readFully(data); buffer.put(data); } - offset += sizes[mip]; - mipWidth = Math.max(mipWidth / 2, 1); mipHeight = Math.max(mipHeight / 2, 1); } @@ -561,6 +555,153 @@ public class DDSLoader implements AssetLoader { return buffer; } + /** + * Reads a grayscale image with mipmaps from the InputStream + * @param flip Flip the loaded image by Y axis + * @param totalSize Total size of the image in bytes including the mipmaps + * @return A ByteBuffer containing the grayscale image data with mips. + * @throws java.io.IOException If an error occured while reading from InputStream + */ + public ByteBuffer readGrayscale3D(boolean flip, int totalSize) throws IOException { + ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize * depth); + + if (bpp == 8) { + logger.finest("Source image format: R8"); + } + + assert bpp == pixelFormat.getBitsPerPixel(); + + + for (int i = 0; i < depth; i++) { + int mipWidth = width; + int mipHeight = height; + + for (int mip = 0; mip < mipMapCount; mip++) { + byte[] data = new byte[sizes[mip]]; + in.readFully(data); + if (flip) { + data = flipData(data, mipWidth * bpp / 8, mipHeight); + } + buffer.put(data); + + mipWidth = Math.max(mipWidth / 2, 1); + mipHeight = Math.max(mipHeight / 2, 1); + } + } + buffer.rewind(); + return buffer; + } + + /** + * Reads an uncompressed RGB or RGBA image. + * + * @param flip Flip the image on the Y axis + * @param totalSize Size of the image in bytes including mipmaps + * @return ByteBuffer containing image data with mipmaps in the format specified by pixelFormat_ + * @throws java.io.IOException If an error occured while reading from InputStream + */ + public ByteBuffer readRGB3D(boolean flip, int totalSize) throws IOException { + int redCount = count(redMask), + blueCount = count(blueMask), + greenCount = count(greenMask), + alphaCount = count(alphaMask); + + if (redMask == 0x00FF0000 && greenMask == 0x0000FF00 && blueMask == 0x000000FF) { + if (alphaMask == 0xFF000000 && bpp == 32) { + logger.finest("Data source format: BGRA8"); + } else if (bpp == 24) { + logger.finest("Data source format: BGR8"); + } + } + + int sourcebytesPP = bpp / 8; + int targetBytesPP = pixelFormat.getBitsPerPixel() / 8; + + ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize * depth); + + for (int k = 0; k < depth; k++) { + // ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize); + int mipWidth = width; + int mipHeight = height; + int offset = k * totalSize; + byte[] b = new byte[sourcebytesPP]; + for (int mip = 0; mip < mipMapCount; mip++) { + for (int y = 0; y < mipHeight; y++) { + for (int x = 0; x < mipWidth; x++) { + in.readFully(b); + + int i = byte2int(b); + + byte red = (byte) (((i & redMask) >> redCount)); + byte green = (byte) (((i & greenMask) >> greenCount)); + byte blue = (byte) (((i & blueMask) >> blueCount)); + byte alpha = (byte) (((i & alphaMask) >> alphaCount)); + + if (flip) { + dataBuffer.position(offset + ((mipHeight - y - 1) * mipWidth + x) * targetBytesPP); + } + //else + // dataBuffer.position(offset + (y * width + x) * targetBytesPP); + + if (alphaMask == 0) { + dataBuffer.put(red).put(green).put(blue); + } else { + dataBuffer.put(red).put(green).put(blue).put(alpha); + } + } + } + + offset += (mipWidth * mipHeight * targetBytesPP); + + mipWidth = Math.max(mipWidth / 2, 1); + mipHeight = Math.max(mipHeight / 2, 1); + } + } + dataBuffer.rewind(); + return dataBuffer; + } + + /** + * Reads a DXT compressed image from the InputStream + * + * @param totalSize Total size of the image in bytes, including mipmaps + * @return ByteBuffer containing compressed DXT image in the format specified by pixelFormat_ + * @throws java.io.IOException If an error occured while reading from InputStream + */ + public ByteBuffer readDXT3D(boolean flip, int totalSize) throws IOException { + logger.finest("Source image format: DXT"); + + ByteBuffer bufferAll = BufferUtils.createByteBuffer(totalSize * depth); + + for (int i = 0; i < depth; i++) { + ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize); + int mipWidth = width; + int mipHeight = height; + for (int mip = 0; mip < mipMapCount; mip++) { + if (flip) { + byte[] data = new byte[sizes[mip]]; + in.readFully(data); + ByteBuffer wrapped = ByteBuffer.wrap(data); + wrapped.rewind(); + ByteBuffer flipped = DXTFlipper.flipDXT(wrapped, mipWidth, mipHeight, pixelFormat); + flipped.rewind(); + buffer.put(flipped); + } else { + byte[] data = new byte[sizes[mip]]; + in.readFully(data); + buffer.put(data); + } + + mipWidth = Math.max(mipWidth / 2, 1); + mipHeight = Math.max(mipHeight / 2, 1); + } + buffer.rewind(); + bufferAll.put(buffer); + } + + return bufferAll; + } + /** * Reads the image data from the InputStream in the required format. * If the file contains a cubemap image, it is loaded as 6 ByteBuffers @@ -583,19 +724,28 @@ public class DDSLoader implements AssetLoader { } ArrayList allMaps = new ArrayList(); - if (depth > 1){ - for (int i = 0; i < depth; i++){ + if (depth > 1 && !texture3D) { + for (int i = 0; i < depth; i++) { if (compressed) { - allMaps.add(readDXT2D(flip,totalSize)); + allMaps.add(readDXT2D(flip, totalSize)); } else if (grayscaleOrAlpha) { allMaps.add(readGrayscale2D(flip, totalSize)); } else { allMaps.add(readRGB2D(flip, totalSize)); } } + } else if (texture3D) { + if (compressed) { + allMaps.add(readDXT3D(flip, totalSize)); + } else if (grayscaleOrAlpha) { + allMaps.add(readGrayscale3D(flip, totalSize)); + } else { + allMaps.add(readRGB3D(flip, totalSize)); + } + } else { if (compressed) { - allMaps.add(readDXT2D(flip,totalSize)); + allMaps.add(readDXT2D(flip, totalSize)); } else if (grayscaleOrAlpha) { allMaps.add(readGrayscale2D(flip, totalSize)); } else { diff --git a/engine/src/core/com/jme3/asset/TextureKey.java b/engine/src/core/com/jme3/asset/TextureKey.java index 5abb52981..7dc9ee071 100644 --- a/engine/src/core/com/jme3/asset/TextureKey.java +++ b/engine/src/core/com/jme3/asset/TextureKey.java @@ -29,7 +29,6 @@ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - package com.jme3.asset; import com.jme3.export.JmeExporter; @@ -39,6 +38,7 @@ import com.jme3.export.OutputCapsule; import com.jme3.texture.Image; import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; +import com.jme3.texture.Texture3D; import com.jme3.texture.TextureCubeMap; import java.io.IOException; import java.nio.ByteBuffer; @@ -48,23 +48,24 @@ public class TextureKey extends AssetKey { private boolean generateMips; private boolean flipY; private boolean asCube; + private boolean asTexture3D; private int anisotropy; - public TextureKey(String name, boolean flipY){ + public TextureKey(String name, boolean flipY) { super(name); this.flipY = flipY; } - public TextureKey(String name){ + public TextureKey(String name) { super(name); this.flipY = true; } - public TextureKey(){ + public TextureKey() { } @Override - public String toString(){ + public String toString() { return name + (flipY ? " (Flipped)" : "") + (asCube ? " (Cube)" : "") + (generateMips ? " (Mipmaped)" : ""); } @@ -73,39 +74,43 @@ public class TextureKey extends AssetKey { * @return true to enable smart cache */ @Override - public boolean useSmartCache(){ + public boolean useSmartCache() { return true; } @Override - public Object createClonedInstance(Object asset){ + public Object createClonedInstance(Object asset) { Texture tex = (Texture) asset; return tex.createSimpleClone(); } @Override - public Object postProcess(Object asset){ + public Object postProcess(Object asset) { Image img = (Image) asset; - if (img == null) + if (img == null) { return null; + } Texture tex; - if (isAsCube()){ - if (isFlipY()){ + if (isAsCube()) { + if (isFlipY()) { // also flip -y and +y image in cubemap ByteBuffer pos_y = img.getData(2); img.setData(2, img.getData(3)); img.setData(3, pos_y); } tex = new TextureCubeMap(); - }else{ + } else if (isAsTexture3D()) { + tex = new Texture3D(); + } else { tex = new Texture2D(); } // enable mipmaps if image has them // or generate them if requested by user - if (img.hasMipmaps() || isGenerateMips()) + if (img.hasMipmaps() || isGenerateMips()) { tex.setMinFilter(Texture.MinFilter.Trilinear); + } tex.setAnisotropicFilter(getAnisotropy()); tex.setName(getName()); @@ -141,15 +146,23 @@ public class TextureKey extends AssetKey { this.generateMips = generateMips; } + public boolean isAsTexture3D() { + return asTexture3D; + } + + public void setAsTexture3D(boolean asTexture3D) { + this.asTexture3D = asTexture3D; + } + @Override - public boolean equals(Object other){ - if (!(other instanceof TextureKey)){ + public boolean equals(Object other) { + if (!(other instanceof TextureKey)) { return false; } - return super.equals(other) && isFlipY() == ((TextureKey)other).isFlipY(); + return super.equals(other) && isFlipY() == ((TextureKey) other).isFlipY(); } - public void write(JmeExporter ex) throws IOException{ + public void write(JmeExporter ex) throws IOException { super.write(ex); OutputCapsule oc = ex.getCapsule(this); oc.write(flipY, "flip_y", false); @@ -159,7 +172,7 @@ public class TextureKey extends AssetKey { } @Override - public void read(JmeImporter im) throws IOException{ + public void read(JmeImporter im) throws IOException { super.read(im); InputCapsule ic = im.getCapsule(this); flipY = ic.readBoolean("flip_y", false); diff --git a/engine/src/core/com/jme3/material/Material.java b/engine/src/core/com/jme3/material/Material.java index fa14f0d12..3a692efb8 100644 --- a/engine/src/core/com/jme3/material/Material.java +++ b/engine/src/core/com/jme3/material/Material.java @@ -368,9 +368,8 @@ public class Material implements Asset, Cloneable, Savable, Comparable } if (type != null && paramDef.getVarType() != type) { - logger.logp(Level.WARNING, "Material parameter being set: {0} with " - + "type {1} doesn't match definition type {2}", - name, type.name(), paramDef.getVarType()); + logger.log(Level.WARNING, "Material parameter being set: {0} with " + + "type {1} doesn't match definition type {2}", new Object[]{name, type.name(), paramDef.getVarType()} ); } return newName; @@ -497,7 +496,7 @@ public class Material implements Asset, Cloneable, Savable, Comparable return; } - VarType paramType = null; + VarType paramType = null; switch (value.getType()) { case TwoDimensional: paramType = VarType.Texture2D; diff --git a/engine/src/test-data/Textures/3D/flame.dds b/engine/src/test-data/Textures/3D/flame.dds new file mode 100644 index 0000000000000000000000000000000000000000..2eb00f74621731b6dac8a24baaafb4a2a2167439 GIT binary patch literal 131200 zcmeHwVQ^bjmZlhPYQ-@D&A=bn4+`Oe+4<%jOwHk<9%+Q)5U@L&Ab z7O>eI_}{Dial`%V;omJCZT^4rS8MVAx=SvdzPRImYWerHb?|=`_|Y!3+VKXf%N`g& z<+v|P-xGvyS7NyZl8e_vJt2~<{=L`!{_ARO_z&T)A^JM}Eh;0vS@_W|zdzzXKXHqn zEl_9&8Uox3|iSSD`2WzwH2h}9kcShXfH5r~S7Vh>qLebn3UB}-a z8rrj)WXs@fjOQu@f5b~4590i<=3iC=G z@p*sO9%B8F_@loR^Qqa!BZ&AXlYYXlz7y;{vx)uv`U7);k>uNO zy}ef@65ed`ILCjJgArF(R~P4lF+Wrh`M?UnKMnza-u|CGZ{Mb-CwRTzaP&}f_(#!R zvUiLR!m*`Ul8gSJTl<(~%hb-1v3zDYbKF-$czT!r=9q14mgoP) zsZ?_pBn0R>;vWiy+DVr9myWO5#^aLz7xf?O_u3rUtjPaPs8xHb*V%1^XRG@3f!^NU z0LkP1>l~}A!v6|NiwYPpy9a_t%Afb^F33$N!Cm_fPEH z*~|7n4*B5SuplITFq?i~xA!Sy{2|?!CH(sH5pTxd!14Z{X;b-h+F3={|G9AM$==@F z7|9JsZ4O8Wmc)OG(lbKWuiN+=?eS*IWs1MYVE@luT<5B(@$vjWpiSqU&T5YT2DA_J z(H+A6-#=k<8RP$mf3x-T8vmZr|37y==+fK&Hf>+t=WFD6f4^2!0Q=98eOE6B zVf{D2>p_esApDc@|25)qv;Ac;{!_Abgy+`_k(LHu4bS(#)@rpwRaK0CM!WEF$GI`~ z|L61ZmW~dQ4?um|H8R5WA2<7ZZrI-|Q~w`?d_UBCF#bO)tb=@CHRJ!T_T5Ys( z$~%|N9VC5e*6atOupl70&kOe7-X`-uCbfU#dOsB?#s3~HK2crmbP}FW7+Y0#~iN8YQb;JD8HR_jfy^r|U)g_bkzV~SLDJLZSNj|Cl zX2t1rvV2N=e&t~9T#T-NM>~Bem($@7&ufDOFkLs>_h0k-l(GM>ON#of>Abtysf+)g z*S4xQyOZm|lG?4R&32ISKamf#h}%%du58halFQdA^)q} z5883wCh6ZyUQxXl6SKE96rr7Y?_jrvz1_`7X3+dJ>*@C11jHVDX{ zdq4)bg@(CK0QsRxX!Rh;7!LqHu#4m``}e=H-#5+&6)Gd1>WJc}cmVaG zI{hK92e5v0Y&u^OA9Qte@O8$oLdVbS-+#$o0lv%lxM7y-&c5>=$#~sqpL;|vcj)4O z&t`Z(80$NGM#JTo-*n z0P$=%tY!({g@D_U0YV7L$OqmN6a0_jd{K_|o)wHg@IjJL{Z4cLfF>$w zU#-74jz9W;s0T|DzK`C!>fc}K?>`p+{{JSC@9{3*bu1D&9Vfhhe{9SC2Or$Y z_K))gz|Z*0c;DRq-KgJ)m!b^i3E%NS==Y?{_d1qeeQRBno%w&yo?O2d=wDp#jqAO( zHnHBb6`yCF^^S2oDosZG^Fso!?fq_tKO^{{ujgNx0{$QKLA=XV34ovZUN~RKcwda4 z)%6GG^SV0W|6A32_g2^V(&Qgn)%oE8*Z?Bg-ddsk04Il?4JYeb4>sEtBdC zO&dwxr=C5~*DLtGlY?%D-v7(^&sy=HQ9mD>_tZAPhCku`K%sj1_#I;Y|3TrI`RF;| zcwF{>t*YDA6Yk)6zytaO^`7O&|0)xIZ^RqzU*G@jzVkNR?`QiT)HbTn|AYCU|67|W zsOf!ug#Z18cwnHnm*WA%KcvWf5RH#64Z_7A+?`;#`fa%6hL z2GWmrwTFTKFZw^!YMTe6-~i}4_7ed2&-tKAA78V5zcJoV1xoqs9{BvF{Qrp({y+2s$m{w4UoQj# zxn9Bl$LEzL|G!M}2e@pbK-edLJ{gDnuTCFcyJ!RczslENyBKuq`Tws>!1`Yo@5%MQ zwb%cMpT2*C2lV6f_*=&N%@*&-_p!Ds~{9C2C3dS9gn_&}FsoDU#aP)N4=_w)OgY5aGk??XJl`;LeY5&w-%nJmSN zad)t`H!#Qhfkph4U4Kv4uk?I*9qa{u%rah=@KYFHBR+UNtIHQA0RMQrn)8ELKOSnA z_1|W}(}>@dcaVIgzbCJo z%l^@RmF!{8|JQfA9ZFKv1J`#Zy#IMX^#8{5zqPg13xqfJ|LKUvxF4j%*If8s36F%^ z?MSBuzjJap?mY|}z=ZFX{q>#7(2SkrdAJ_x5%!My1N9&he~ZS8(C-1xe@k<{-}?Qj z7X5h;hkrF6->{)EL-^l*AXsw#1L7ZUFT?)-7RB>Q_HMj>W~&?O!9@KZocHSWRVT&> z@5$kXz=oIJ;rMWTI9NLni*fyj8{>s`$^WA9GLEm=3xV9u zNA9QZ8SkHK0eYyPf8D!(;qI!c_!0WPGsC6l{erqcKfs|~blqy-FWOpK2c{`K2Z&Aojw?&ppA(xrE=6jtP zOf`?j`k4QL_8;B7TlRahsQn}U`tzT`zt{S8{Xg!SULWhxraAxrwKuj$M(pAe|IPRqf5-CKRBWJ^@jv@3Pc7{C z^L}qppOl^+(g%sZx$!sp|MC7({GElLoS4@2|M=^|eJ5ivvHyG5v31xFT;%^jpN2ju z3`zW9AmGPp`hOeb|NXlD@6N(2DgFBYp9|Gzf&bY{_&#>bR;$M!^{KmiMB;Cec))1? zTlRY#*`(+?bQyghql{U?o&+4ZYv|J~gRz3+*< z+tmODPjW`{6>6Mz8_CaW&06p5>=?-p6na~X{(sl5zvq4dX8-<5dq3m)ECTzziju4& zJio2=4f>z~0LkxaPpzaI?OgBmu9jShojb_*e_K1aBibwC!ALVcPv!$@{LH3b#`%8X zZpioR`aSwBKq+(&hAv{FL^S6Z&`%`QPo(5SFf+?fWbJ zef}-{>*~b({}kX4%RSEjA^uG>Gsg)}><6&07UT2(9@bJN{Qr8%|2Gw%<4XD-g#10g zubcmk{Qv&E&F*CUpVz*r>iPd~*e>y{_RQ@>b)+@xE@rNY*Qr91wbwwe2!%7_pfME2T8{I z4`oX911QSc!5m#Lt~cHWKCsot%Od%F#`g=pFZOq+Sr+p<)_-q&N$dv$ztg>;I{g8~ zbIA8xT`jyra>5;P^~84aeo(~aajdNgK2QMSgE0Ki{a5>b@_w){UA%Rp$0G=M)#>Vg z5%2jk_w#zc&mF1#9dwH&JQ4q$j|{6MBmO`KV|_sT?+HtO2#wE;#_!8M-wk;ET(=*D z_--HWWc!b}HrLvw9_Rco_J5f9?mNu?LH_}KAYLzg+4qsZe{=00;KO>L_eft6-y@I! z*2_yvOKjg*|937X?VHBO>iUZQx*HCJqVF;G1Bioyh^V1+55Uj(Ck7K(0plNs2aN8CedVqyMf+i~YWZtsV!^e;NPZAL#3!o}S=(@UyA;{;c}Kx9IzB+Z%Dmbn^k`|J&Qe zeqV8Zkmo^KMjwjxpjqL+p1l}vPWu}Cgm-`bCr6cNj^~5N=HtzKbn)P&egIvv{`(8= zznjfJ;QXD>=SK+dhND53em&T#{`jaZvB3MmkDagq|5s=4@cZpR0HNPE+xN-$`TY11 z{WtW3V0-`Z(Nwd)!B<0g{_Fhj59HQ9eu3n9Jpa2*@I8w8A(;=bjK9A$Kf~)@_E+N( zj|2KWF#qG5Z_E#-kL+Xq*FPP*ckf?*_b>kg-T&%aHb*-UfJv74f4+U0&Ho@?@0`lB zzTR~#SgYs%)@bj)o1Yf^-(MCUI~Lj_*8i`bh`2jCe#G@%IDdfqm;T=(^FQL%JtF!w z+on0qya{sE+@6N8VNo`C+zye4bqYi}=}W_HKM1wEv-z5svrZ zJoj2X-ut!x0{A=G|L@lxDC7pBeE#!PKGJ-5Z!hPAaDQ0xf31Um5bz%v`d@W4e}BDj zDBy#Jk0fu?GAp0FEzWb#Xde}J&W>^ZXE|?gh5Z5U2aD@JXa~*pAx3?Zvbo{|58ws$ zevJQ{1y4WF|2g0DuJ+JMdU_+D@Aa5B%@g(mAdNcvwsPN?#v>{E@cyc4_|>iT@I?p9|Z) zNy7IYZ9C%6@!m=8g_SC2W16m?1pH@b&#jVtk2XF9h0!aJ#XRohr@56S!TZZE`xy#B-IwaNNGg~rF|Z_OrS{IBQhZ^!`=#%7!+(^*8eeC9DAV!y?C)!}-g|N)?)9ZbJ?LDE$I;d<&T~TWzKiigEsxBL^FfRKBjG?ex-Rj& zxp-dfcn1OhjH;@H7uxs67eqXma0eW#znQv&u8$8!T-j`96UmPruy?+`FEK`PeZ*68 z9xTomiX!R@ulD`q{Wn*C5HEc^5C?szX%PIc-%ri8WE#ZzFlVP0?uLF~9fbcvhX?sU zJtX7v^!tSp&zpj5~P`m*DH zvw#1Rzt-`Aq5n^2($GA1kL$m0Pb4Dha+=SByJfn*wWlYxKzNV$dmQ@y zV8-W5+1&bo{#()UKknV{alvtajJJ3B%&>aAk@G$M>eB~$bo>wO7X{nJa=mZSd|)5o z@Ar#-Kld#jY61Ia|9^ks4%OGx#PQ$0!mDrX(Dehw^S>nDL-xuKcBy4 zQqKqbi^AU=jSlp3z90L;!2Hkpfcm744DBL)k>guBzGfQ_#7m#=YdGpI>HqMv!hbwk zQ{!a*=g$g97JG9C4-&qwosW2e7-)m<~^>AU+n)5YQM;5 zeBa=B@FA_H@Gt+eI!pL}KOgbx^8v;6q%t%_*X4MB{&;`Yp6b^YkXg<|5lU?=l_cQj^i76|3BsRIFzLv@AqK7zbD+z@xYh; z`||fcWB$2DJg+pK#dt2OWNAGY0X&_$_#MxGwomKN|AzYBm^km@O3#KGW{`QU1w zFYhOM5oAJsa`4wB8f^J`cq2kk9uMp19s~-jSXmxz7dX zyM@DVlZ^3xNNeJJKrvsCUrNyRqJFf$E&IQl)ISvQSCpRUI+$K!4#n`xr!^_?EaBhWCAWQ-p|sw(FPlj4u|TjxJKM0jC*|J=r= z49N*^#MS7WQAtjoDbNtw|<)f2f)&G+#iH;UylE*{{5By{>6N+?)>kD zqlcPnY6KtX=>rQ}Asxhcr<&c4j;J^fuDJeFLbCqbBJn_RJqQFqvgggPo`ZCdpX9Tt z`fa{V|DOAUAD*|jhQosYi}}ITRUza20qH?rFI2icnf?0_FW`UG5#F~BA?)?04M>KbkuHUvl;)3%&*&m=jLBCL$|FMYuqrcblKfLOpKpF^Ogzs|U zr;C{xJI8w;=6`y!{dXH!?#~C^VQ2_M_apwJ(D0FDv+IAQzaQGa9{<<#b6b5r(GTQX z+8xU_*zhMj(}h;mF_zEBh2n!jPc*==iRkQX!uF^)@r{{ zH#OFCz6bgNs5^QCvvmFOMK|>S5b+<{e@{<$H(fXT_gDJ+c`n6&Ny2MqKHk!x>;LwY z_CR5sj{l$0nzbE|kB!m&e^H3HK>Zib2dGaYBZ`dwEaLxz5dT5@H;(_`EUX*U_xpKQ zdvYZ`)5z<;ceSVB{P!H=f1%Jf9E}S8H$Jajo)4jreVE<9%xwkylS#(^hg$uCn(As^ z|DDo)yVBIOf%|_HwBKs6SZs~H@7LPBC-nSpd>-TjW&Xz^_K$e!?C>U#^& z?_qjo<~ZSf68QfI$HvZ)e2@0Zi$?tA`rkV7ANuRMq}cB{pAWQ@^8fquzzAXdpVubx zwy_-de*pe}ORhJ{>%RyDfNiq>yJh@8#_Lf3l_h(gkGmau{{M3qOZopOkFACH`zGQ3 zE*;+ZyiuKO_W4h~U+MnymDcYb!1o$$a2-8((Qp=CY?s!y}0PJ)8HFe=pV#Yg1D! zSI7r|c3(ciZ+tNB zhI0PxB+o%VfKVvI^+DJlVC4Q;t_Mc_(ZcflFB&ha%?I_JfWNN(`|Omx^^w>@j=rZa zG8a%r*I35$-v@N(zqw&enV0z$%f$QWpR=l{|F{$Qe}2EH2fgKJeP=E=a5v$f=(le> zH#KvZWbFUaW#IpW;D@eTP5&$HebHZMGos#qemGd0O#0dX-*R-aAKHJtLwKHv+gnv_ zX_n;r&WNicx`+EeVn4BVc^hQ1nE9S(;_HC_ALV*b)GwLu6ZucG>jTdJHCge#+xwyZQ&$h{Rk!y; zJvh$?t?JXMXm5`F|Mq^HZMV+fUs~_U`Je2^?D!ks&ja?G$%y_h_btYoeQDAE-?9A6 zu|VJUe)_(}IP(2P|38VpwbuJKuzy&talCS<8DuyRl<MG9vyngbTWBsqc zeuA#g54s&d_hP<}#J_YrjrL`B`I)yowb1^B=l?sFy>E2?={GjQd*kvglYf{C1W2}T zgY}=z{>}b<^8HHn(*yn<=pR|a^Xyd6)u7A&yjghteEzuT|MbhkEys556#ZY`e=7+6 zU&5U4zhwWypUCkp9dEOZ2gd6|LnEgM|M%bWxH8An9Pd4(wVw3Vi2a_2w3-6+|H{$* zd-D-5#Q(e=2*P|4YM1Lj8XvRaZG0aO;D73PH|OiVUKlGm|2?Cd359~Gee z!x()ZoEHrJU!sElU-Ty-$^W;={$G**pA`AtS}idQ{BQ2}Q{?|oi}T-3YR|8XuC22D z4{EbJfd0=iJ`eIi91j}xP0Hq)59YRFK1kqyk9PVH(EmB#a|-z1&MG0F1bKE=^#6ED z`*8{Xf8p+8{|D{|C-F8H-bOqS|GK(llKlCT+WL7gf1dxb-$Ub(<0@T0h5Y~7bKLL! zJKACi|9=6V*VQKb0n>OH$J=Z&?)U5Y|0h1h|8M;?|KFzP|9eXK|3NqK|J!B!Zx#G; zy$1an6?)(C;ZpwpPYwKkluP;lwi5oo+4Nh!&-I;uP+wrXfb|7s+3!z`ht#+0t8bECOSe2!`Sr5rDuFeKla{H4?fsSnZ~Oz#hHJ*O-m z=6s)=Ps(&X^BMlQ$p6!w2ZQH(Dj~u52!eiecen4Rd=biU9xV5ZFX~T7p8rDQV|M#- zgHP7ig9HD^k*yQ_FF5ZN>U%!%()UztLwnFxNk)B1FU7e2AMppe{|H?-`}bG+`yu{N z?%PdxA)mwN^VN{Ne*epX|Nh;-;`5*2{AX{tE6Vu8`ENET2j_m!Mf+Eji}d@IKE7uE z{-VFv)q8I4kGN`T(mem)zW?Q|PN#F3@SLBBdsm}jw)ceBZ9BKRhx5TWAGC$9Uq2DE zkDt}w5ApYBM%e!GJkO?#s0Rdp;CA}N`M>sUK?juk^pZZHy$_9yNPqts@s!#11M%xt zvL6wiZjk+%Ec5-4-|h2>egNbBL07D=Z;tLqdmkCu#rz+$e{ZF~AL6Z#_fS5w_TSjw zqd&+hqY1(n{X4`1JpZ2=UI@HiUBmSucLHBL6yDACpOrqoR{#El7vgy+04MzI+xpyD zEyeYoiT=5knskQsrM`2W4eCF+9?0t7U+M3U^+3S?%M$*ZJ5wzmo<99D$@bQ{mT&Bv z_*;@O{*Oj?%X(jn&i_VzNB^JE^#kaOB)l)f58=HKh`8qF`ua%5`=Q)lA=&smDO)Rl zn1F;prlEoJdAIL9)cnB*r_T_+_Et}A9~}5ZvIp`9-6N{Be~a3`F&@PAAH;hc-)-4@ zXJ_Qh8OFb_GvSQ^0i5v1`9jwFmyiFO1N?#h$MIiPY_dOc`t&-&yDB!<5{vB&kc{|u z>G)sL-&@Py8}UN>hx1?A{_p$a{9rzxWZs&@Sx6uB`JnGDM%>Wu ziShqW+8t_U#>wZueoNb_=5oEv_x#?;xfVVDzqlS~Z)ZNKQNN^YuK5AuKLr$k?Du<% zo?89-Z$R6iR#(?>{_oAg2j_cr^Zl(S-L=pkPWF4Yki8e{moL9B`s*w-1S9;vpLe?& zAOIkFoA!%*gRhbK-`liCRUiK!x#$M}FZzLb;CbDnqjEpUBKm-MWwD<9^82O>HZSyh z;COEk_};6dJ-pwW(f+m&%?bYRzI>!*V4!qAaAasm`hSbyA93S+B>FwQS$Mev!hgp9 zr1tBTe0rMs;O}b5m7Pl>{`I*xJ$(`HP1Q{BL}o#NWL58}$+K()0fjPd)$tB=G$=Y;dLtZ|wgt zHrvbleb6u97^H)F{kH(m>uQtrpEO>^@iv|KIGa@nz`!liIDS&9;H@e_m@< zZDV@=f1o8d5aswEpC|eM*0Fz>{!8jX#)oYVKLmt?|9$5@F8fTjMAq~F@%}E|{vSS1 v^1;RUna$p>^gb9L=