From 8c4556877288d44c555c6b45e79b11741d0e09bc Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Sun, 12 Jul 2015 11:14:22 +0100 Subject: [PATCH 01/94] Added intellij files to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 09d505187..6858b0c8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.gradle/ /.nb-gradle/private/ /.nb-gradle/profiles/private/ +/.idea/ /dist/ /build/ /netbeans/ @@ -124,6 +125,7 @@ *.so *.jnilib *.dylib +*.iml /sdk/dist/ !/jme3-bullet-native/libs/native/windows/x86_64/bulletjme.dll !/jme3-bullet-native/libs/native/windows/x86/bulletjme.dll From 2080453430726ece8ae05b4d9b5a3da4aacfb173 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Sun, 12 Jul 2015 19:18:18 +0100 Subject: [PATCH 02/94] Changed build scripts slightly for bullet to not reference src outside it's own base dir. This was mainly done to be able to import the project correctly in IntelliJ IDEA. The modules will now instead reference each other through the dependency mechanism. --- jme3-bullet-native-android/build.gradle | 2 +- jme3-jbullet/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jme3-bullet-native-android/build.gradle b/jme3-bullet-native-android/build.gradle index 30fdd44b5..a640fcf93 100644 --- a/jme3-bullet-native-android/build.gradle +++ b/jme3-bullet-native-android/build.gradle @@ -21,6 +21,7 @@ if (!hasProperty('mainClass')) { } dependencies { + compile project(':jme3-bullet-native') compile project(':jme3-bullet') } @@ -28,7 +29,6 @@ dependencies { sourceSets { main { java { - srcDir jmeCppPath srcDir jmeAndroidPath } } diff --git a/jme3-jbullet/build.gradle b/jme3-jbullet/build.gradle index bcf1660bc..763a9e57b 100644 --- a/jme3-jbullet/build.gradle +++ b/jme3-jbullet/build.gradle @@ -6,7 +6,6 @@ sourceSets { main { java { srcDir 'src/main/java' - srcDir '../jme3-bullet/src/common/java' } } } @@ -16,4 +15,5 @@ dependencies { compile files('../lib/jbullet.jar', '../lib/stack-alloc.jar') compile project(':jme3-core') compile project(':jme3-terrain') + compile project(':jme3-bullet') } From 9059eb30e4a0e95404ac953495f2ad1fa0ac47b5 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Tue, 14 Jul 2015 22:28:13 +0100 Subject: [PATCH 03/94] Added support for setting minification and magnification filters on a texture in the j3m material file. This also adds support for double and single quoted paths as well as being able to set WrapMode for a specific WrapAxis. This resolves #295 --- .../com/jme3/material/plugins/J3MLoader.java | 311 +++++++++++++++--- 1 file changed, 264 insertions(+), 47 deletions(-) diff --git a/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java b/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java index d0620ca59..29829b099 100644 --- a/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java +++ b/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java @@ -43,7 +43,6 @@ import com.jme3.math.Vector3f; import com.jme3.shader.Shader; import com.jme3.shader.VarType; import com.jme3.texture.Texture; -import com.jme3.texture.Texture.WrapMode; import com.jme3.texture.Texture2D; import com.jme3.texture.image.ColorSpace; import com.jme3.util.PlaceholderAssets; @@ -52,10 +51,13 @@ import com.jme3.util.blockparser.Statement; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class J3MLoader implements AssetLoader { @@ -126,59 +128,153 @@ public class J3MLoader implements AssetLoader { technique.setShadowMode(sm); } - private Object readValue(VarType type, String value) throws IOException{ - if (type.isTextureType()){ -// String texturePath = readString("[\n;(//)(\\})]"); + private List tokenizeTextureValue(final String value) { + final List matchList = new ArrayList(); + final Pattern regex = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); + final Matcher regexMatcher = regex.matcher(value.trim()); + + while (regexMatcher.find()) { + if (regexMatcher.group(1) != null) { + matchList.add(regexMatcher.group(1)); + } else if (regexMatcher.group(2) != null) { + matchList.add(regexMatcher.group(2)); + } else { + matchList.add(regexMatcher.group()); + } + } + + return matchList; + } + + private List parseTextureOptions(final List values) { + final List matchList = new ArrayList(); + + if (values.isEmpty() || values.size() == 1) { + return matchList; + } + + // Loop through all but the last value, the last one is going to be the path. + for (int i = 0; i < values.size() - 1; i++) { + final String value = values.get(i); + final TextureOption textureOption = TextureOption.getTextureOption(value); + + if (textureOption == null && !value.contains("\\") && !value.contains("/")) { + logger.log(Level.WARNING, "Unknown texture option \"{0}\" encountered for \"{1}\" in material \"{2}\"", new Object[]{value, key, material.getKey().getName()}); + } else if (textureOption != null){ + final String option = textureOption.getOptionValue(value); + matchList.add(new TextureOptionValue(textureOption, option)); + } + } + + return matchList; + } + + private boolean isTexturePathDeclaredTheTraditionalWay(final int numberOfValues, final int numberOfTextureOptions, final String texturePath) { + return (numberOfValues > 1 && (texturePath.startsWith("Flip Repeat ") || texturePath.startsWith("Flip ") || + texturePath.startsWith("Repeat ") || texturePath.startsWith("Repeat Flip "))) || numberOfTextureOptions == 0; + } + + private Texture parseTextureType(final VarType type, final String value) { + final List textureValues = tokenizeTextureValue(value); + final List textureOptionValues = parseTextureOptions(textureValues); + + TextureKey textureKey = null; + boolean repeat = false; + + // If there is only one token on the value, it must be the path to the texture. + if (textureValues.size() == 1) { + textureKey = new TextureKey(textureValues.get(0)); + } else { String texturePath = value.trim(); boolean flipY = false; - boolean repeat = false; - if (texturePath.startsWith("Flip Repeat ")){ - texturePath = texturePath.substring(12).trim(); - flipY = true; - repeat = true; - }else if (texturePath.startsWith("Flip ")){ - texturePath = texturePath.substring(5).trim(); - flipY = true; - }else if (texturePath.startsWith("Repeat ")){ - texturePath = texturePath.substring(7).trim(); - repeat = true; - } - TextureKey texKey = new TextureKey(texturePath, flipY); - switch (type) { - case Texture3D: - texKey.setTextureTypeHint(Texture.Type.ThreeDimensional); - break; - case TextureArray: - texKey.setTextureTypeHint(Texture.Type.TwoDimensionalArray); - break; - case TextureCubeMap: - texKey.setTextureTypeHint(Texture.Type.CubeMap); - break; + // If there are no valid "new" texture options specified but the path is split into several parts, lets parse the old way. + if (isTexturePathDeclaredTheTraditionalWay(textureValues.size(), textureOptionValues.size(), texturePath)) { + if (texturePath.startsWith("Flip Repeat ") || texturePath.startsWith("Repeat Flip ")) { + texturePath = texturePath.substring(12).trim(); + flipY = true; + repeat = true; + } else if (texturePath.startsWith("Flip ")) { + texturePath = texturePath.substring(5).trim(); + flipY = true; + } else if (texturePath.startsWith("Repeat ")) { + texturePath = texturePath.substring(7).trim(); + repeat = true; + } + + // Support path starting with quotes (double and single) + if (texturePath.startsWith("\"") || texturePath.startsWith("'")) { + texturePath = texturePath.substring(1); + } + + // Support path ending with quotes (double and single) + if (texturePath.endsWith("\"") || texturePath.endsWith("'")) { + texturePath = texturePath.substring(0, texturePath.length() - 1); + } + + textureKey = new TextureKey(texturePath, flipY); } - texKey.setGenerateMips(true); - - Texture tex; - try { - tex = assetManager.loadTexture(texKey); - } catch (AssetNotFoundException ex){ - logger.log(Level.WARNING, "Cannot locate {0} for material {1}", new Object[]{texKey, key}); - tex = null; + + if (textureKey == null) { + textureKey = new TextureKey(textureValues.get(textureValues.size() - 1)); } - if (tex != null){ - if (repeat){ - tex.setWrap(WrapMode.Repeat); - } - }else{ - tex = new Texture2D(PlaceholderAssets.getPlaceholderImage(assetManager)); - if (repeat){ - tex.setWrap(WrapMode.Repeat); + + // Apply texture options to the texture key + if (!textureOptionValues.isEmpty()) { + for (final TextureOptionValue textureOptionValue : textureOptionValues) { + textureOptionValue.applyToTextureKey(textureKey); } - tex.setKey(texKey); - tex.setName(texKey.getName()); } - return tex; - }else{ + } + + switch (type) { + case Texture3D: + textureKey.setTextureTypeHint(Texture.Type.ThreeDimensional); + break; + case TextureArray: + textureKey.setTextureTypeHint(Texture.Type.TwoDimensionalArray); + break; + case TextureCubeMap: + textureKey.setTextureTypeHint(Texture.Type.CubeMap); + break; + } + + textureKey.setGenerateMips(true); + + Texture texture; + + try { + texture = assetManager.loadTexture(textureKey); + } catch (AssetNotFoundException ex){ + logger.log(Level.WARNING, "Cannot locate {0} for material {1}", new Object[]{textureKey, key}); + texture = null; + } + + if (texture == null){ + texture = new Texture2D(PlaceholderAssets.getPlaceholderImage(assetManager)); + texture.setKey(textureKey); + texture.setName(textureKey.getName()); + } + + // This is here for backwards compatibility, we need to do this after the texture has been instantiated. + if (repeat) { + texture.setWrap(Texture.WrapMode.Repeat); + } + + // Apply texture options to the texture + if (!textureOptionValues.isEmpty()) { + for (final TextureOptionValue textureOptionValue : textureOptionValues) { + textureOptionValue.applyToTexture(texture); + } + } + + return texture; + } + + private Object readValue(final VarType type, final String value) throws IOException{ + if (type.isTextureType()) { + return parseTextureType(type, value); + } else { String[] split = value.trim().split(whitespacePattern); switch (type){ case Float: @@ -619,4 +715,125 @@ public class J3MLoader implements AssetLoader { } } + /** + * Texture options allow you to specify how a texture should be initialized by including an option before + * the path to the texture in the .j3m file. + *

+ * Example: + *

+     *     DiffuseMap: MinTrilinear MagBilinear WrapRepeat_S "some/path/to a/texture.png"
+     *     
+ * This would apply a minification filter of "Trilinear", a magnification filter of "Bilinear" and set the wrap mode to "Repeat". + *

+ *

+ * Note: If several filters of the same type are added, eg. MinTrilinear MinNearestLinearMipMap, the last one will win. + *

+ */ + private enum TextureOption { + + /** + * Applies a {@link com.jme3.texture.Texture.MinFilter} to the texture. + */ + Min { + @Override + public void applyToTexture(final String option, final Texture texture) { + texture.setMinFilter(Texture.MinFilter.valueOf(option)); + } + }, + + /** + * Applies a {@link com.jme3.texture.Texture.MagFilter} to the texture. + */ + Mag { + @Override + public void applyToTexture(final String option, final Texture texture) { + texture.setMagFilter(Texture.MagFilter.valueOf(option)); + } + }, + + /** + * Applies a {@link com.jme3.texture.Texture.WrapMode} to the texture. This also supports {@link com.jme3.texture.Texture.WrapAxis} + * by adding "_AXIS" to the texture option. For instance if you wanted to repeat on the S (horizontal) axis, you + * would use
WrapRepeat_S
as a texture option. + */ + Wrap { + @Override + public void applyToTexture(final String option, final Texture texture) { + final int separatorPosition = option.indexOf("_"); + + if (separatorPosition >= option.length() - 2) { + final String axis = option.substring(separatorPosition + 1); + final String mode = option.substring(0, separatorPosition); + final Texture.WrapAxis wrapAxis = Texture.WrapAxis.valueOf(axis); + texture.setWrap(wrapAxis, Texture.WrapMode.valueOf(mode)); + } else { + texture.setWrap(Texture.WrapMode.valueOf(option)); + } + } + }, + + /** + * Applies a {@link com.jme3.texture.Texture.WrapMode#Repeat} to the texture. This is simply an alias for + * WrapRepeat, please use WrapRepeat instead if possible as this may become deprecated later on. + */ + Repeat { + @Override + public void applyToTexture(final String option, final Texture texture) { + Wrap.applyToTexture("Repeat", texture); + } + }, + + /** + * Applies flipping on the Y axis to the {@link TextureKey#setFlipY(boolean)}. + */ + Flip { + @Override + public void applyToTextureKey(final String option, final TextureKey textureKey) { + textureKey.setFlipY(true); + } + }; + + public String getOptionValue(final String option) { + return option.substring(name().length()); + } + + public void applyToTexture(final String option, final Texture texture) { + } + + public void applyToTextureKey(final String option, final TextureKey textureKey) { + } + + public static TextureOption getTextureOption(final String option) { + for(final TextureOption textureOption : TextureOption.values()) { + if (option.startsWith(textureOption.name())) { + return textureOption; + } + } + + return null; + } + } + + /** + * Internal object used for holding a {@link com.jme3.material.plugins.J3MLoader.TextureOption} and it's value. Also + * contains a couple of convenience methods for applying the TextureOption to either a TextureKey or a Texture. + */ + private static class TextureOptionValue { + + private final TextureOption textureOption; + private final String value; + + public TextureOptionValue(TextureOption textureOption, String value) { + this.textureOption = textureOption; + this.value = value; + } + + public void applyToTextureKey(final TextureKey textureKey) { + textureOption.applyToTextureKey(value, textureKey); + } + + public void applyToTexture(final Texture texture) { + textureOption.applyToTexture(value, texture); + } + } } From 0869880c8e0167e4a765ab05b7b1f85a455e20bd Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Wed, 15 Jul 2015 10:24:04 +0100 Subject: [PATCH 04/94] Reverted changes to build script regarding bullet libs. --- jme3-bullet-native-android/build.gradle | 3 ++- jme3-jbullet/build.gradle | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/jme3-bullet-native-android/build.gradle b/jme3-bullet-native-android/build.gradle index a640fcf93..3aa000c1e 100644 --- a/jme3-bullet-native-android/build.gradle +++ b/jme3-bullet-native-android/build.gradle @@ -21,7 +21,7 @@ if (!hasProperty('mainClass')) { } dependencies { - compile project(':jme3-bullet-native') +// compile project(':jme3-bullet-native') compile project(':jme3-bullet') } @@ -29,6 +29,7 @@ dependencies { sourceSets { main { java { + srcDir jmeCppPath srcDir jmeAndroidPath } } diff --git a/jme3-jbullet/build.gradle b/jme3-jbullet/build.gradle index 763a9e57b..0e3967ab2 100644 --- a/jme3-jbullet/build.gradle +++ b/jme3-jbullet/build.gradle @@ -6,6 +6,7 @@ sourceSets { main { java { srcDir 'src/main/java' + srcDir '../jme3-bullet/src/common/java' } } } @@ -15,5 +16,5 @@ dependencies { compile files('../lib/jbullet.jar', '../lib/stack-alloc.jar') compile project(':jme3-core') compile project(':jme3-terrain') - compile project(':jme3-bullet') +// compile project(':jme3-bullet') } From be692a2ceb183734653d6d5167f43562efbe871a Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Wed, 15 Jul 2015 10:25:27 +0100 Subject: [PATCH 05/94] Reverted changes to build script regarding bullet libs. --- jme3-bullet-native-android/build.gradle | 2 +- jme3-jbullet/build.gradle | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/jme3-bullet-native-android/build.gradle b/jme3-bullet-native-android/build.gradle index a640fcf93..30fdd44b5 100644 --- a/jme3-bullet-native-android/build.gradle +++ b/jme3-bullet-native-android/build.gradle @@ -21,7 +21,6 @@ if (!hasProperty('mainClass')) { } dependencies { - compile project(':jme3-bullet-native') compile project(':jme3-bullet') } @@ -29,6 +28,7 @@ dependencies { sourceSets { main { java { + srcDir jmeCppPath srcDir jmeAndroidPath } } diff --git a/jme3-jbullet/build.gradle b/jme3-jbullet/build.gradle index 763a9e57b..0e3967ab2 100644 --- a/jme3-jbullet/build.gradle +++ b/jme3-jbullet/build.gradle @@ -6,6 +6,7 @@ sourceSets { main { java { srcDir 'src/main/java' + srcDir '../jme3-bullet/src/common/java' } } } @@ -15,5 +16,5 @@ dependencies { compile files('../lib/jbullet.jar', '../lib/stack-alloc.jar') compile project(':jme3-core') compile project(':jme3-terrain') - compile project(':jme3-bullet') +// compile project(':jme3-bullet') } From e33d2539edbac1adebe0bf34b318f2d1bde2ae4c Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Wed, 15 Jul 2015 13:08:36 +0100 Subject: [PATCH 06/94] Added additional convenience constructors to Light, AmbientLight, DirectionalLight, PointLight and SpotLight as mentioned in #297 --- .../java/com/jme3/light/AmbientLight.java | 16 ++++++++ .../java/com/jme3/light/DirectionalLight.java | 27 ++++++++++++ .../src/main/java/com/jme3/light/Light.java | 16 ++++++++ .../main/java/com/jme3/light/PointLight.java | 36 ++++++++++++++++ .../main/java/com/jme3/light/SpotLight.java | 41 +++++++++++++++++-- 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/light/AmbientLight.java b/jme3-core/src/main/java/com/jme3/light/AmbientLight.java index e147c6590..fb52301f9 100644 --- a/jme3-core/src/main/java/com/jme3/light/AmbientLight.java +++ b/jme3-core/src/main/java/com/jme3/light/AmbientLight.java @@ -32,6 +32,7 @@ package com.jme3.light; import com.jme3.bounding.BoundingBox; +import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; import com.jme3.scene.Spatial; @@ -49,6 +50,21 @@ import com.jme3.util.TempVars; */ public class AmbientLight extends Light { + /** + * Default constructor for AmbientLight. + */ + public AmbientLight() { + } + + /** + * Constructor which allows setting of the color. + * + * @param color the color to apply to this light. + */ + public AmbientLight(final ColorRGBA color) { + super(color); + } + @Override public boolean intersectsBox(BoundingBox box, TempVars vars) { return true; diff --git a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java index c4258a67f..7f1541fda 100644 --- a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java +++ b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java @@ -36,6 +36,7 @@ import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; +import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; import com.jme3.scene.Spatial; @@ -53,6 +54,32 @@ public class DirectionalLight extends Light { protected Vector3f direction = new Vector3f(0f, -1f, 0f); + /** + * Default constructor for DirectionalLight. Direction will be defaulted to -1 on the Y axis. + */ + public DirectionalLight() { + } + + /** + * Constructor which allows setting of the direction. + * + * @param direction the direction vector for the light. + */ + public DirectionalLight(final Vector3f direction) { + this.direction = direction; + } + + /** + * Constructor which allows setting of the color and direction. + * + * @param color the color to apply to this light. + * @param direction the direction vector for the light. + */ + public DirectionalLight(final ColorRGBA color, final Vector3f direction) { + super(color); + this.direction = direction; + } + @Override public void computeLastDistance(Spatial owner) { lastDistance = 0; // directional lights are always closest to their owner diff --git a/jme3-core/src/main/java/com/jme3/light/Light.java b/jme3-core/src/main/java/com/jme3/light/Light.java index 4217e1b62..7738c1721 100644 --- a/jme3-core/src/main/java/com/jme3/light/Light.java +++ b/jme3-core/src/main/java/com/jme3/light/Light.java @@ -115,6 +115,22 @@ public abstract class Light implements Savable, Cloneable { boolean frustumCheckNeeded = true; boolean intersectsFrustum = false; + /** + * Default constructor for Light. + */ + public Light() { + + } + + /** + * Constructor which allows setting of the color. + * + * @param color the color to apply to this light. + */ + public Light(final ColorRGBA color) { + this.color = color; + } + /** * Returns the color of the light. * diff --git a/jme3-core/src/main/java/com/jme3/light/PointLight.java b/jme3-core/src/main/java/com/jme3/light/PointLight.java index 55a129275..1e93e1f1e 100644 --- a/jme3-core/src/main/java/com/jme3/light/PointLight.java +++ b/jme3-core/src/main/java/com/jme3/light/PointLight.java @@ -38,6 +38,7 @@ import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; +import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.math.Plane; import com.jme3.math.Vector3f; @@ -62,6 +63,41 @@ public class PointLight extends Light { protected float radius = 0; protected float invRadius = 0; + /** + * Default constructor for PointLight. + *

+ *

    + *
  • Position will be defaulted to 0,0,0.
  • + *
  • Radius will be defaulted to 0
  • + *
+ *

+ */ + public PointLight() { + } + + /** + * Constructor which allows setting of the color and position. + * + * @param color the color to apply to this light. + * @param position the position of the light. + */ + public PointLight(final ColorRGBA color, final Vector3f position) { + this(color, position, 0); + } + + /** + * Constructor which allows setting of the color, position and radius. + * + * @param color the color to apply to this light. + * @param position the position of the light. + * @param radius the radius of the light. + */ + public PointLight(final ColorRGBA color, final Vector3f position, final float radius) { + super(color); + this.position = position; + this.radius = radius; + } + @Override public void computeLastDistance(Spatial owner) { if (owner.getWorldBound() != null) { diff --git a/jme3-core/src/main/java/com/jme3/light/SpotLight.java b/jme3-core/src/main/java/com/jme3/light/SpotLight.java index e6443df7c..b4531fb1c 100644 --- a/jme3-core/src/main/java/com/jme3/light/SpotLight.java +++ b/jme3-core/src/main/java/com/jme3/light/SpotLight.java @@ -34,6 +34,7 @@ package com.jme3.light; import com.jme3.bounding.BoundingBox; import com.jme3.bounding.BoundingVolume; import com.jme3.export.*; +import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.math.Plane; import com.jme3.math.Vector3f; @@ -51,8 +52,8 @@ import java.io.IOException; * can be used to attenuate the influence of the light depending on the * distance between the light and the effected object. * Also the angle of the cone can be tweaked by changing the spot inner angle and the spot outer angle. - * the spot inner angle determin the cone of light where light has full influence. - * the spot outer angle determin the cone global cone of light of the spot light. + * the spot inner angle determine the cone of light where light has full influence. + * the spot outer angle determine the cone global cone of light of the spot light. * the light intensity slowly decrease between the inner cone and the outer cone. * @author Nehon */ @@ -64,16 +65,48 @@ public class SpotLight extends Light { protected float spotOuterAngle = FastMath.QUARTER_PI / 6; protected float spotRange = 100; protected float invSpotRange = 1f / 100; - protected float packedAngleCos=0; + protected float packedAngleCos = 0; protected float outerAngleCosSqr, outerAngleSinSqr; protected float outerAngleSinRcp, outerAngleSin, outerAngleCos; - + + /** + * Default constructor for SpotLight. + *

+ *

    + *
  • Position will be defaulted to 0,0,0
  • + *
  • Direction will be defaulted to -1 on the Y axis
  • + *
  • Range will be defaulted to 100
  • + *
+ *

+ */ public SpotLight() { super(); computeAngleParameters(); } + /** + * Constructor which allows setting color, position, direction, inner angle, outer angle and range. + * + * @param color the color of the spotlight. + * @param position the position of the spotlight. + * @param direction the direction of the spotlight. + * @param spotInnerAngle the inner angle of the spotlight. + * @param spotOuterAngle the outer angle of the spotlight. + * @param spotRange the range of the spotlight. + */ + public SpotLight(final ColorRGBA color, final Vector3f position, final Vector3f direction, final float spotInnerAngle, + final float spotOuterAngle, final float spotRange) + { + super(color); + this.position = position; + this.direction = direction; + this.spotInnerAngle = spotInnerAngle; + this.spotOuterAngle = spotOuterAngle; + this.spotRange = spotRange; + computeAngleParameters(); + } + private void computeAngleParameters() { float innerCos = FastMath.cos(spotInnerAngle); outerAngleCos = FastMath.cos(spotOuterAngle); From d319a7c5d31ffebcefc4aac7a52ac73665906049 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Mon, 20 Jul 2015 11:22:16 +0100 Subject: [PATCH 07/94] Added unit test for J3MLoader to cover the new texture parameters available in #295. Also fixed a couple of issues in the code to reduce logging that was not needed and removed redundant code. This update also updates junit to 4.12 and adds Mockito and Fest Assertions as test dependencies. --- common.gradle | 4 +- jme3-core/build.gradle | 5 + .../com/jme3/material/plugins/J3MLoader.java | 17 +-- .../jme3/material/plugins/J3MLoaderTest.java | 117 ++++++++++++++++++ .../resources/texture-parameters-newstyle.j3m | 11 ++ .../resources/texture-parameters-oldstyle.j3m | 6 + 6 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java create mode 100644 jme3-core/src/test/resources/texture-parameters-newstyle.j3m create mode 100644 jme3-core/src/test/resources/texture-parameters-oldstyle.j3m diff --git a/common.gradle b/common.gradle index 6af4c664f..43fcf481d 100644 --- a/common.gradle +++ b/common.gradle @@ -21,7 +21,9 @@ repositories { dependencies { // Adding dependencies here will add the dependencies to each subproject. - testCompile group: 'junit', name: 'junit', version: '4.10' + testCompile group: 'junit', name: 'junit', version: '4.12' + testCompile group: 'org.mockito', name: 'mockito-core', version: '2.0.28-beta' + testCompile group: 'org.easytesting', name: 'fest-assert-core', version: '2.0M10' } javadoc { diff --git a/jme3-core/build.gradle b/jme3-core/build.gradle index bd699f52a..87b52fb94 100644 --- a/jme3-core/build.gradle +++ b/jme3-core/build.gradle @@ -10,6 +10,11 @@ sourceSets { srcDir 'src/tools/java' } } + test { + java { + srcDir 'src/test/java' + } + } } buildscript { diff --git a/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java b/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java index 29829b099..13a8e0232 100644 --- a/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java +++ b/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java @@ -158,7 +158,7 @@ public class J3MLoader implements AssetLoader { final String value = values.get(i); final TextureOption textureOption = TextureOption.getTextureOption(value); - if (textureOption == null && !value.contains("\\") && !value.contains("/")) { + if (textureOption == null && !value.contains("\\") && !value.contains("/") && !values.get(0).equals("Flip") && !values.get(0).equals("Repeat")) { logger.log(Level.WARNING, "Unknown texture option \"{0}\" encountered for \"{1}\" in material \"{2}\"", new Object[]{value, key, material.getKey().getName()}); } else if (textureOption != null){ final String option = textureOption.getOptionValue(value); @@ -179,27 +179,25 @@ public class J3MLoader implements AssetLoader { final List textureOptionValues = parseTextureOptions(textureValues); TextureKey textureKey = null; - boolean repeat = false; // If there is only one token on the value, it must be the path to the texture. if (textureValues.size() == 1) { - textureKey = new TextureKey(textureValues.get(0)); + textureKey = new TextureKey(textureValues.get(0), false); } else { String texturePath = value.trim(); - boolean flipY = false; // If there are no valid "new" texture options specified but the path is split into several parts, lets parse the old way. if (isTexturePathDeclaredTheTraditionalWay(textureValues.size(), textureOptionValues.size(), texturePath)) { + boolean flipY = false; + if (texturePath.startsWith("Flip Repeat ") || texturePath.startsWith("Repeat Flip ")) { texturePath = texturePath.substring(12).trim(); flipY = true; - repeat = true; } else if (texturePath.startsWith("Flip ")) { texturePath = texturePath.substring(5).trim(); flipY = true; } else if (texturePath.startsWith("Repeat ")) { texturePath = texturePath.substring(7).trim(); - repeat = true; } // Support path starting with quotes (double and single) @@ -216,7 +214,7 @@ public class J3MLoader implements AssetLoader { } if (textureKey == null) { - textureKey = new TextureKey(textureValues.get(textureValues.size() - 1)); + textureKey = new TextureKey(textureValues.get(textureValues.size() - 1), false); } // Apply texture options to the texture key @@ -256,11 +254,6 @@ public class J3MLoader implements AssetLoader { texture.setName(textureKey.getName()); } - // This is here for backwards compatibility, we need to do this after the texture has been instantiated. - if (repeat) { - texture.setWrap(Texture.WrapMode.Repeat); - } - // Apply texture options to the texture if (!textureOptionValues.isEmpty()) { for (final TextureOptionValue textureOptionValue : textureOptionValues) { diff --git a/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java b/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java new file mode 100644 index 000000000..6438baaac --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java @@ -0,0 +1,117 @@ +package com.jme3.material.plugins; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetKey; +import com.jme3.asset.AssetManager; +import com.jme3.asset.TextureKey; +import com.jme3.material.MatParamTexture; +import com.jme3.material.Material; +import com.jme3.material.MaterialDef; +import com.jme3.shader.VarType; +import com.jme3.texture.Texture; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Daniel Johansson + * @since 2015-07-20 + */ +@RunWith(MockitoJUnitRunner.class) +public class J3MLoaderTest { + + private J3MLoader j3MLoader; + + @Mock + private AssetInfo assetInfo; + + @Mock + private AssetManager assetManager; + + @Mock + private AssetKey assetKey; + + @Mock + private MaterialDef materialDef; + + @Before + public void setUp() throws Exception { + when(assetKey.getExtension()).thenReturn(".j3m"); + when(assetInfo.getManager()).thenReturn(assetManager); + when(assetInfo.getKey()).thenReturn(assetKey); + when(assetManager.loadAsset(any(AssetKey.class))).thenReturn(materialDef); + + j3MLoader = new J3MLoader(); + } + + @Test + public void oldStyleTextureParameters_shouldBeSupported() throws Exception { + when(assetInfo.openStream()).thenReturn(J3MLoader.class.getResourceAsStream("/texture-parameters-oldstyle.j3m")); + + final Texture textureOldStyle = Mockito.mock(Texture.class); + final Texture textureOldStyleUsingQuotes = Mockito.mock(Texture.class); + + final TextureKey textureKeyUsingQuotes = setupMockForTexture("OldStyleUsingQuotes", "old style using quotes/texture.png", true, textureOldStyleUsingQuotes); + final TextureKey textureKeyOldStyle = setupMockForTexture("OldStyle", "old style/texture.png", true, textureOldStyle); + + j3MLoader.load(assetInfo); + + verify(assetManager).loadTexture(textureKeyUsingQuotes); + verify(assetManager).loadTexture(textureKeyOldStyle); + verify(textureOldStyle).setWrap(Texture.WrapMode.Repeat); + verify(textureOldStyleUsingQuotes).setWrap(Texture.WrapMode.Repeat); + } + + @Test + public void newStyleTextureParameters_shouldBeSupported() throws Exception { + when(assetInfo.openStream()).thenReturn(J3MLoader.class.getResourceAsStream("/texture-parameters-newstyle.j3m")); + + final Texture textureNoParameters = Mockito.mock(Texture.class); + final Texture textureFlip = Mockito.mock(Texture.class); + final Texture textureRepeat = Mockito.mock(Texture.class); + final Texture textureRepeatAxis = Mockito.mock(Texture.class); + final Texture textureMin = Mockito.mock(Texture.class); + final Texture textureMag = Mockito.mock(Texture.class); + final Texture textureCombined = Mockito.mock(Texture.class); + + final TextureKey textureKeyNoParameters = setupMockForTexture("Empty", "empty.png", false, textureNoParameters); + final TextureKey textureKeyFlip = setupMockForTexture("Flip", "flip.png", true, textureFlip); + setupMockForTexture("Repeat", "repeat.png", false, textureRepeat); + setupMockForTexture("RepeatAxis", "repeat-axis.png", false, textureRepeatAxis); + setupMockForTexture("Min", "min.png", false, textureMin); + setupMockForTexture("Mag", "mag.png", false, textureMag); + setupMockForTexture("Combined", "combined.png", true, textureCombined); + + j3MLoader.load(assetInfo); + + verify(assetManager).loadTexture(textureKeyNoParameters); + verify(assetManager).loadTexture(textureKeyFlip); + + verify(textureRepeat).setWrap(Texture.WrapMode.Repeat); + verify(textureRepeatAxis).setWrap(Texture.WrapAxis.T, Texture.WrapMode.Repeat); + verify(textureMin).setMinFilter(Texture.MinFilter.Trilinear); + verify(textureMag).setMagFilter(Texture.MagFilter.Bilinear); + + verify(textureCombined).setMagFilter(Texture.MagFilter.Nearest); + verify(textureCombined).setMinFilter(Texture.MinFilter.BilinearNoMipMaps); + verify(textureCombined).setWrap(Texture.WrapMode.Repeat); + } + + private TextureKey setupMockForTexture(final String paramName, final String path, final boolean flipY, final Texture texture) { + when(materialDef.getMaterialParam(paramName)).thenReturn(new MatParamTexture(VarType.Texture2D, paramName, texture, 0, null)); + + final TextureKey textureKey = new TextureKey(path, flipY); + textureKey.setGenerateMips(true); + + when(assetManager.loadTexture(textureKey)).thenReturn(texture); + + return textureKey; + } +} diff --git a/jme3-core/src/test/resources/texture-parameters-newstyle.j3m b/jme3-core/src/test/resources/texture-parameters-newstyle.j3m new file mode 100644 index 000000000..a7619d948 --- /dev/null +++ b/jme3-core/src/test/resources/texture-parameters-newstyle.j3m @@ -0,0 +1,11 @@ +Material Test : matdef.j3md { + MaterialParameters { + Empty: "empty.png" + Flip: Flip "flip.png" + Repeat: WrapRepeat "repeat.png" + Min: MinTrilinear "min.png" + Mag: MagBilinear "mag.png" + RepeatAxis: WrapRepeat_T "repeat-axis.png" + Combined: MagNearest MinBilinearNoMipMaps Flip WrapRepeat "combined.png" + } +} \ No newline at end of file diff --git a/jme3-core/src/test/resources/texture-parameters-oldstyle.j3m b/jme3-core/src/test/resources/texture-parameters-oldstyle.j3m new file mode 100644 index 000000000..7f34af464 --- /dev/null +++ b/jme3-core/src/test/resources/texture-parameters-oldstyle.j3m @@ -0,0 +1,6 @@ +Material Test : matdef.j3md { + MaterialParameters { + OldStyle: Flip Repeat old style/texture.png + OldStyleUsingQuotes: Repeat Flip "old style using quotes/texture.png" + } +} \ No newline at end of file From df19c742486711cd34b849d0e29d3cbd913d5b99 Mon Sep 17 00:00:00 2001 From: Dokthar Date: Wed, 22 Jul 2015 22:36:05 +0200 Subject: [PATCH 08/94] SDK scenecomposer : changed the lineWidth of the axisMarker - lineWidth of the arrows is now set at 3f - the arrows of the axisMarker are now easier to select. --- .../src/com/jme3/gde/scenecomposer/SceneEditTool.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneEditTool.java b/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneEditTool.java index 372984f8e..9c7ad75cb 100644 --- a/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneEditTool.java +++ b/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneEditTool.java @@ -453,6 +453,9 @@ public abstract class SceneEditTool { Geometry arrowX = new Geometry("arrowX", new Arrow(new Vector3f(arrowSize, 0, 0))); Geometry arrowY = new Geometry("arrowY", new Arrow(new Vector3f(0, arrowSize, 0))); Geometry arrowZ = new Geometry("arrowZ", new Arrow(new Vector3f(0, 0, arrowSize))); + arrowX.getMesh().setLineWidth(3f); + arrowY.getMesh().setLineWidth(3f); + arrowZ.getMesh().setLineWidth(3f); axis.attachChild(arrowX); axis.attachChild(arrowY); axis.attachChild(arrowZ); From 89f10eca58afd4c9645b60f8781e5578fa92b2c8 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Wed, 26 Aug 2015 22:24:24 +0100 Subject: [PATCH 09/94] Added jme3-lwjgl3 module which ultimately adds support for LWJGL 3.x and GLFW. --- common.gradle | 3 + .../resources/joystick-mapping.properties | 6 + .../com/jme3/system/NativeLibraryLoader.java | 22 +- .../system/lwjgl/LwjglAbstractDisplay.java | 2 +- .../com/jme3/system/lwjgl/LwjglContext.java | 28 +- jme3-lwjgl3/build.gradle | 12 + .../java/com/jme3/audio/lwjgl/LwjglAL.java | 140 +++++ .../java/com/jme3/audio/lwjgl/LwjglALC.java | 58 +++ .../java/com/jme3/audio/lwjgl/LwjglEFX.java | 66 +++ .../jme3/input/lwjgl/GlfwJoystickInput.java | 213 ++++++++ .../com/jme3/input/lwjgl/LwjglKeyInput.java | 116 +++++ .../com/jme3/input/lwjgl/LwjglMouseInput.java | 189 +++++++ .../java/com/jme3/renderer/lwjgl/LwjglGL.java | 458 ++++++++++++++++ .../com/jme3/renderer/lwjgl/LwjglGLExt.java | 83 +++ .../jme3/renderer/lwjgl/LwjglGLFboEXT.java | 99 ++++ .../jme3/renderer/lwjgl/LwjglGLFboGL3.java | 97 ++++ .../com/jme3/system/lwjgl/LwjglCanvas.java | 369 +++++++++++++ .../com/jme3/system/lwjgl/LwjglContext.java | 289 +++++++++++ .../com/jme3/system/lwjgl/LwjglDisplay.java | 12 + .../lwjgl/LwjglGLDebugOutputHandler.java | 78 +++ .../system/lwjgl/LwjglOffscreenBuffer.java | 14 + .../system/lwjgl/LwjglSmoothingTimer.java | 203 ++++++++ .../com/jme3/system/lwjgl/LwjglTimer.java | 139 +++++ .../com/jme3/system/lwjgl/LwjglWindow.java | 489 ++++++++++++++++++ settings.gradle | 1 + 25 files changed, 3158 insertions(+), 28 deletions(-) create mode 100644 jme3-lwjgl3/build.gradle create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglAL.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java diff --git a/common.gradle b/common.gradle index 6af4c664f..da38174d3 100644 --- a/common.gradle +++ b/common.gradle @@ -17,6 +17,9 @@ repositories { maven { url "http://nifty-gui.sourceforge.net/nifty-maven-repo" } + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + } } dependencies { diff --git a/jme3-core/src/main/resources/joystick-mapping.properties b/jme3-core/src/main/resources/joystick-mapping.properties index d7ce2f50a..966832036 100644 --- a/jme3-core/src/main/resources/joystick-mapping.properties +++ b/jme3-core/src/main/resources/joystick-mapping.properties @@ -63,3 +63,9 @@ Xbox\ 360\ Wireless\ Receiver.AXIS_RX=z Xbox\ 360\ Wireless\ Receiver.AXIS_RY=rz Xbox\ 360\ Wireless\ Receiver.z=AXIS_RX Xbox\ 360\ Wireless\ Receiver.rz=AXIS_RY + +# Microsoft PC-joystick driver +Microsoft\ PC-joystick\ driver.12=POV +Y +Microsoft\ PC-joystick\ driver.13=POV +X +Microsoft\ PC-joystick\ driver.14=POV -Y +Microsoft\ PC-joystick\ driver.15=POV -X diff --git a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java index 2917350f6..6e83e0e6b 100644 --- a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java +++ b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java @@ -48,9 +48,8 @@ import java.util.logging.Logger; /** * Utility class to register, extract, and load native libraries. *
- * Register your own libraries via the - * {@link #registerNativeLibrary(java.lang.String, com.jme3.system.Platform, java.lang.String, boolean) } - * method, for each platform. + * Register your own libraries via the {@link #registerNativeLibrary(String, Platform, String, String)} method, for + * each platform. * You can then extract this library (depending on platform), by * using {@link #loadNativeLibrary(java.lang.String, boolean) }. *
@@ -125,23 +124,6 @@ public final class NativeLibraryLoader { } static { - // LWJGL - registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/lwjgl.dll"); - registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/lwjgl64.dll"); - registerNativeLibrary("lwjgl", Platform.Linux32, "native/linux/liblwjgl.so"); - registerNativeLibrary("lwjgl", Platform.Linux64, "native/linux/liblwjgl64.so"); - registerNativeLibrary("lwjgl", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); - registerNativeLibrary("lwjgl", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); - - // OpenAL - // For OSX: Need to add lib prefix when extracting - registerNativeLibrary("openal", Platform.Windows32, "native/windows/OpenAL32.dll"); - registerNativeLibrary("openal", Platform.Windows64, "native/windows/OpenAL64.dll"); - registerNativeLibrary("openal", Platform.Linux32, "native/linux/libopenal.so"); - registerNativeLibrary("openal", Platform.Linux64, "native/linux/libopenal64.so"); - registerNativeLibrary("openal", Platform.MacOSX32, "native/macosx/openal.dylib", "libopenal.dylib"); - registerNativeLibrary("openal", Platform.MacOSX64, "native/macosx/openal.dylib", "libopenal.dylib"); - // BulletJme registerNativeLibrary("bulletjme", Platform.Windows32, "native/windows/x86/bulletjme.dll"); registerNativeLibrary("bulletjme", Platform.Windows64, "native/windows/x86_64/bulletjme.dll"); diff --git a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java index 7cdca0a57..415084788 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java +++ b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java @@ -40,7 +40,6 @@ import com.jme3.input.lwjgl.JInputJoyInput; import com.jme3.input.lwjgl.LwjglKeyInput; import com.jme3.input.lwjgl.LwjglMouseInput; import com.jme3.system.AppSettings; -import com.jme3.system.JmeContext.Type; import com.jme3.system.JmeSystem; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; @@ -207,6 +206,7 @@ public abstract class LwjglAbstractDisplay extends LwjglContext implements Runna + "Must set with JmeContext.setSystemListner()."); } + registerNatives(); loadNatives(); logger.log(Level.FINE, "Using LWJGL {0}", Sys.getVersion()); if (!initInThread()) { diff --git a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java index c88f7b734..70885c39b 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java +++ b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -52,12 +52,8 @@ import com.jme3.renderer.opengl.GLRenderer; import com.jme3.renderer.opengl.GLTiming; import com.jme3.renderer.opengl.GLTimingState; import com.jme3.renderer.opengl.GLTracer; -import com.jme3.system.AppSettings; -import com.jme3.system.JmeContext; -import com.jme3.system.JmeSystem; -import com.jme3.system.NativeLibraryLoader; -import com.jme3.system.SystemListener; -import com.jme3.system.Timer; +import com.jme3.system.*; + import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -166,7 +162,25 @@ public abstract class LwjglContext implements JmeContext { } } } - + + protected void registerNatives() { + // LWJGL + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/lwjgl.dll"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/lwjgl64.dll"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux32, "native/linux/liblwjgl.so"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux64, "native/linux/liblwjgl64.so"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); + + // For OSX: Need to add lib prefix when extracting + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows32, "native/windows/OpenAL32.dll"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows64, "native/windows/OpenAL64.dll"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux32, "native/linux/libopenal.so"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux64, "native/linux/libopenal64.so"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX32, "native/macosx/openal.dylib", "libopenal.dylib"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX64, "native/macosx/openal.dylib", "libopenal.dylib"); + } + protected void loadNatives() { if (JmeSystem.isLowPermissions()) { return; diff --git a/jme3-lwjgl3/build.gradle b/jme3-lwjgl3/build.gradle new file mode 100644 index 000000000..c9b43e0de --- /dev/null +++ b/jme3-lwjgl3/build.gradle @@ -0,0 +1,12 @@ +if (!hasProperty('mainClass')) { + ext.mainClass = '' +} + +dependencies { + compile project(':jme3-core') + compile project(':jme3-desktop') + compile 'org.lwjgl:lwjgl:3.0.0b-SNAPSHOT' + compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0b-SNAPSHOT', classifier: 'natives-windows' + compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0b-SNAPSHOT', classifier: 'natives-linux' + compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0b-SNAPSHOT', classifier: 'natives-osx' +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglAL.java b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglAL.java new file mode 100644 index 000000000..313bdd5ee --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglAL.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */package com.jme3.audio.lwjgl; + +import com.jme3.audio.openal.AL; +import com.jme3.system.NativeLibraryLoader; +import com.jme3.system.Platform; +import org.lwjgl.openal.AL10; +import org.lwjgl.openal.AL11; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public final class LwjglAL implements AL { + + public LwjglAL() { + } + + public String alGetString(int parameter) { + return AL10.alGetString(parameter); + } + + public int alGenSources() { + return AL10.alGenSources(); + } + + public int alGetError() { + return AL10.alGetError(); + } + + public void alDeleteSources(int numSources, IntBuffer sources) { + if (sources.position() != 0) throw new AssertionError(); + if (sources.limit() != numSources) throw new AssertionError(); + AL10.alDeleteSources(sources); + } + + public void alGenBuffers(int numBuffers, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numBuffers) throw new AssertionError(); + AL10.alGenBuffers(buffers); + } + + public void alDeleteBuffers(int numBuffers, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numBuffers) throw new AssertionError(); + AL10.alDeleteBuffers(buffers); + } + + public void alSourceStop(int source) { + AL10.alSourceStop(source); + } + + public void alSourcei(int source, int param, int value) { + AL10.alSourcei(source, param, value); + } + + public void alBufferData(int buffer, int format, ByteBuffer data, int size, int frequency) { + if (data.position() != 0) throw new AssertionError(); + if (data.limit() != size) throw new AssertionError(); + AL10.alBufferData(buffer, format, data, frequency); + } + + public void alSourcePlay(int source) { + AL10.alSourcePlay(source); + } + + public void alSourcePause(int source) { + AL10.alSourcePause(source); + } + + public void alSourcef(int source, int param, float value) { + AL10.alSourcef(source, param, value); + } + + public void alSource3f(int source, int param, float value1, float value2, float value3) { + AL10.alSource3f(source, param, value1, value2, value3); + } + + public int alGetSourcei(int source, int param) { + return AL10.alGetSourcei(source, param); + } + + public void alSourceUnqueueBuffers(int source, int numBuffers, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numBuffers) throw new AssertionError(); + AL10.alSourceUnqueueBuffers(source, buffers); + } + + public void alSourceQueueBuffers(int source, int numBuffers, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numBuffers) throw new AssertionError(); + AL10.alSourceQueueBuffers(source, buffers); + } + + public void alListener(int param, FloatBuffer data) { + AL10.alListenerfv(param, data); + } + + public void alListenerf(int param, float value) { + AL10.alListenerf(param, value); + } + + public void alListener3f(int param, float value1, float value2, float value3) { + AL10.alListener3f(param, value1, value2, value3); + } + + public void alSource3i(int source, int param, int value1, int value2, int value3) { + AL11.alSource3i(source, param, value1, value2, value3); + } + +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java new file mode 100644 index 000000000..e0aed80ee --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java @@ -0,0 +1,58 @@ +package com.jme3.audio.lwjgl; + +import com.jme3.audio.openal.ALC; +import org.lwjgl.openal.ALC10; +import org.lwjgl.openal.ALContext; + +import java.nio.IntBuffer; + +import static org.lwjgl.openal.ALC10.alcGetContextsDevice; +import static org.lwjgl.openal.ALC10.alcGetCurrentContext; + +public class LwjglALC implements ALC { + + private ALContext context; + + public void createALC() { + context = ALContext.create(); + } + + public void destroyALC() { + if (context != null) { + context.destroy(); + } + } + + public boolean isCreated() { + return context != null; + } + + public String alcGetString(final int parameter) { + final long context = alcGetCurrentContext(); + final long device = alcGetContextsDevice(context); + return ALC10.alcGetString(device, parameter); + } + + public boolean alcIsExtensionPresent(final String extension) { + final long context = alcGetCurrentContext(); + final long device = alcGetContextsDevice(context); + return ALC10.alcIsExtensionPresent(device, extension); + } + + public void alcGetInteger(final int param, final IntBuffer buffer, final int size) { + if (buffer.position() != 0) throw new AssertionError(); + if (buffer.limit() != size) throw new AssertionError(); + + final long context = alcGetCurrentContext(); + final long device = alcGetContextsDevice(context); + final int value = ALC10.alcGetInteger(device, param); + //buffer.put(value); + } + + public void alcDevicePauseSOFT() { + } + + public void alcDeviceResumeSOFT() { + } + +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java new file mode 100644 index 000000000..2ddd09ed8 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java @@ -0,0 +1,66 @@ +package com.jme3.audio.lwjgl; + +import com.jme3.audio.openal.EFX; +import org.lwjgl.openal.EXTEfx; + +import java.nio.IntBuffer; + +public class LwjglEFX implements EFX { + + public void alGenAuxiliaryEffectSlots(int numSlots, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numSlots) throw new AssertionError(); + EXTEfx.alGenAuxiliaryEffectSlots(buffers); + } + + public void alGenEffects(int numEffects, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numEffects) throw new AssertionError(); + EXTEfx.alGenEffects(buffers); + } + + public void alEffecti(int effect, int param, int value) { + EXTEfx.alEffecti(effect, param, value); + } + + public void alAuxiliaryEffectSloti(int effectSlot, int param, int value) { + EXTEfx.alAuxiliaryEffectSloti(effectSlot, param, value); + } + + public void alDeleteEffects(int numEffects, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numEffects) throw new AssertionError(); + EXTEfx.alDeleteEffects(buffers); + } + + public void alDeleteAuxiliaryEffectSlots(int numEffectSlots, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numEffectSlots) throw new AssertionError(); + EXTEfx.alDeleteAuxiliaryEffectSlots(buffers); + } + + public void alGenFilters(int numFilters, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numFilters) throw new AssertionError(); + EXTEfx.alGenFilters(buffers); + } + + public void alFilteri(int filter, int param, int value) { + EXTEfx.alFilteri(filter, param, value); + } + + public void alFilterf(int filter, int param, float value) { + EXTEfx.alFilterf(filter, param, value); + } + + public void alDeleteFilters(int numFilters, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numFilters) throw new AssertionError(); + EXTEfx.alDeleteFilters(buffers); + } + + public void alEffectf(int effect, int param, float value) { + EXTEfx.alEffectf(effect, param, value); + } + +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java new file mode 100644 index 000000000..f17c08534 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.input.lwjgl; + +import com.jme3.input.*; +import com.jme3.input.event.JoyAxisEvent; +import com.jme3.input.event.JoyButtonEvent; +import org.lwjgl.opengl.GL11; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.*; + +/** + * @author Daniel Johansson (dannyjo) + * @since 3.1 + */ +public class GlfwJoystickInput implements JoyInput { + + private static final Logger LOGGER = Logger.getLogger(InputManager.class.getName()); + + private boolean initialized = false; + private RawInputListener listener; + private Map joysticks = new HashMap(); + + public void setJoyRumble(int joyId, float amount) { + if (joyId >= joysticks.size()) { + throw new IllegalArgumentException(); + } + } + + @Override + public Joystick[] loadJoysticks(final InputManager inputManager) { + // TODO: Implement + + for (int i = 0; i < GLFW_JOYSTICK_LAST; i++) { + if (glfwJoystickPresent(i) == GL11.GL_TRUE) { + final String name = glfwGetJoystickName(i); + final GlfwJoystick joystick = new GlfwJoystick(inputManager, this, i, name); + joysticks.put(i, joystick); + + final FloatBuffer floatBuffer = glfwGetJoystickAxes(i); + + int axisIndex = 0; + while (floatBuffer.hasRemaining()) { + floatBuffer.get(); + + final String logicalId = JoystickCompatibilityMappings.remapComponent(joystick.getName(), convertAxisIndex(axisIndex)); + final JoystickAxis joystickAxis = new DefaultJoystickAxis(inputManager, joystick, axisIndex, convertAxisIndex(axisIndex), logicalId, true, false, 0.0f); + joystick.addAxis(axisIndex, joystickAxis); + axisIndex++; + } + + final ByteBuffer byteBuffer = glfwGetJoystickButtons(i); + + int buttonIndex = 0; + while (byteBuffer.hasRemaining()) { + byteBuffer.get(); + final String logicalId = JoystickCompatibilityMappings.remapComponent(joystick.getName(), String.valueOf(buttonIndex)); + joystick.addButton(new DefaultJoystickButton(inputManager, joystick, buttonIndex, String.valueOf(buttonIndex), logicalId)); + buttonIndex++; + } + } + } + + return joysticks.values().toArray(new GlfwJoystick[joysticks.size()]); + } + + private String convertAxisIndex(final int index) { + if (index == 0) { + return "pov_x"; + } else if (index == 1) { + return "pov_y"; + } else if (index == 2) { + return "z"; + } else if (index == 3) { + return "rz"; + } + + return String.valueOf(index); + } + + public void initialize() { + initialized = true; + } + + public void update() { + for (final Map.Entry entry : joysticks.entrySet()) { + // Axes + final FloatBuffer axisValues = glfwGetJoystickAxes(entry.getKey()); + + for (final JoystickAxis axis : entry.getValue().getAxes()) { + final float value = axisValues.get(axis.getAxisId()); + listener.onJoyAxisEvent(new JoyAxisEvent(axis, value)); + } + + // Buttons + final ByteBuffer byteBuffer = glfwGetJoystickButtons(entry.getKey()); + + for (final JoystickButton button : entry.getValue().getButtons()) { + final boolean pressed = byteBuffer.get(button.getButtonId()) == GLFW_PRESS; + listener.onJoyButtonEvent(new JoyButtonEvent(button, pressed)); + } + } + } + + public void destroy() { + initialized = false; + } + + public boolean isInitialized() { + return initialized; + } + + public void setInputListener(RawInputListener listener) { + this.listener = listener; + } + + public long getInputTimeNanos() { + return 0; + } + + protected class GlfwJoystick extends AbstractJoystick { + + private JoystickAxis povAxisX; + private JoystickAxis povAxisY; + + public GlfwJoystick(InputManager inputManager, JoyInput joyInput, int joyId, String name) { + super(inputManager, joyInput, joyId, name); + } + + public void addAxis(final int index, final JoystickAxis axis) { + super.addAxis(axis); + + if (index == 0) { + povAxisX = axis; + } else if (index == 1) { + povAxisY = axis; + } + } + + @Override + protected void addButton(JoystickButton button) { + super.addButton(button); + } + + @Override + public JoystickAxis getXAxis() { + return povAxisX; + } + + @Override + public JoystickAxis getYAxis() { + return povAxisY; + } + + @Override + public JoystickAxis getPovXAxis() { + return povAxisX; + } + + @Override + public JoystickAxis getPovYAxis() { + return povAxisY; + } + + @Override + public int getXAxisIndex() { + return povAxisX.getAxisId(); + } + + @Override + public int getYAxisIndex() { + return povAxisY.getAxisId(); + } + } +} + + + diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java new file mode 100644 index 000000000..a39010c95 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.input.lwjgl; + +import com.jme3.input.KeyInput; +import com.jme3.input.RawInputListener; +import com.jme3.input.event.KeyInputEvent; +import com.jme3.system.lwjgl.LwjglTimer; +import com.jme3.system.lwjgl.LwjglWindow; +import org.lwjgl.glfw.GLFWKeyCallback; + +import java.util.LinkedList; +import java.util.Queue; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.*; + +public class LwjglKeyInput implements KeyInput { + + private static final Logger logger = Logger.getLogger(LwjglKeyInput.class.getName()); + + private LwjglWindow context; + private RawInputListener listener; + private boolean initialized; + private GLFWKeyCallback keyCallback; + private Queue keyInputEvents = new LinkedList(); + + public LwjglKeyInput(LwjglWindow context) { + this.context = context; + } + + public void initialize() { + if (!context.isRenderable()) { + return; + } + + glfwSetKeyCallback(context.getWindowHandle(), keyCallback = new GLFWKeyCallback() { + @Override + public void invoke(long window, int key, int scancode, int action, int mods) { + final KeyInputEvent evt = new KeyInputEvent(scancode, (char) key, GLFW_PRESS == action, GLFW_REPEAT == action); + evt.setTime(getInputTimeNanos()); + keyInputEvents.add(evt); + } + }); + + glfwSetInputMode(context.getWindowHandle(), GLFW_STICKY_KEYS, 1); + + initialized = true; + logger.fine("Keyboard created."); + } + + public int getKeyCount() { + return 0; // TODO: How do we figure this out? + } + + public void update() { + if (!context.isRenderable()) { + return; + } + + while (!keyInputEvents.isEmpty()) { + listener.onKeyEvent(keyInputEvents.poll()); + } + } + + public void destroy() { + if (!context.isRenderable()) { + return; + } + + keyCallback.release(); + logger.fine("Keyboard destroyed."); + } + + public boolean isInitialized() { + return initialized; + } + + public void setInputListener(RawInputListener listener) { + this.listener = listener; + } + + public long getInputTimeNanos() { + return (long) (glfwGetTime() * LwjglTimer.LWJGL_TIME_TO_NANOS); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java new file mode 100644 index 000000000..45262ffbc --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.input.lwjgl; + +import com.jme3.cursors.plugins.JmeCursor; +import com.jme3.input.MouseInput; +import com.jme3.input.RawInputListener; +import com.jme3.input.event.MouseButtonEvent; +import com.jme3.input.event.MouseMotionEvent; +import com.jme3.system.lwjgl.LwjglTimer; +import com.jme3.system.lwjgl.LwjglWindow; +import org.lwjgl.glfw.GLFWCursorPosCallback; +import org.lwjgl.glfw.GLFWMouseButtonCallback; +import org.lwjgl.glfw.GLFWScrollCallback; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.Queue; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.*; + +public class LwjglMouseInput implements MouseInput { + + private static final Logger logger = Logger.getLogger(LwjglMouseInput.class.getName()); + + private LwjglWindow context; + private RawInputListener listener; + private boolean cursorVisible = true; + private int mouseX; + private int mouseY; + private int mouseWheel; + private boolean initialized; + private GLFWCursorPosCallback cursorPosCallback; + private GLFWScrollCallback scrollCallback; + private GLFWMouseButtonCallback mouseButtonCallback; + private Queue mouseMotionEvents = new LinkedList(); + private Queue mouseButtonEvents = new LinkedList(); + + public LwjglMouseInput(LwjglWindow context) { + this.context = context; + } + + public void initialize() { + glfwSetCursorPosCallback(context.getWindowHandle(), cursorPosCallback = new GLFWCursorPosCallback() { + @Override + public void invoke(long window, double xpos, double ypos) { + int xDelta; + int yDelta; + int x = (int) Math.round(xpos); + int y = (int) Math.round(ypos); + + if (mouseX == 0) { + mouseX = x; + } + + if (mouseY == 0) { + mouseY = y; + } + + xDelta = x - mouseX; + yDelta = y - mouseY; + mouseX = x; + mouseY = y; + + if (xDelta != 0 || yDelta != 0) { + final MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(x, y * -1, xDelta, yDelta * -1, mouseWheel, 0); + mouseMotionEvent.setTime(getInputTimeNanos()); + mouseMotionEvents.add(mouseMotionEvent); + } + } + }); + + glfwSetScrollCallback(context.getWindowHandle(), scrollCallback = new GLFWScrollCallback() { + @Override + public void invoke(final long window, final double xOffset, final double yOffset) { + mouseWheel += yOffset; + + final MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(mouseX, mouseY, 0, 0, mouseWheel, (int) Math.round(yOffset)); + mouseMotionEvent.setTime(getInputTimeNanos()); + mouseMotionEvents.add(mouseMotionEvent); + } + }); + + glfwSetMouseButtonCallback(context.getWindowHandle(), mouseButtonCallback = new GLFWMouseButtonCallback() { + @Override + public void invoke(final long window, final int button, final int action, final int mods) { + final MouseButtonEvent mouseButtonEvent = new MouseButtonEvent(button, action == GLFW_PRESS, mouseX, mouseY); + mouseButtonEvent.setTime(getInputTimeNanos()); + mouseButtonEvents.add(mouseButtonEvent); + } + }); + + setCursorVisible(cursorVisible); + logger.fine("Mouse created."); + initialized = true; + } + + public boolean isInitialized() { + return initialized; + } + + public int getButtonCount() { + return 2; // TODO: How to determine this? + } + + public void update() { + while (!mouseMotionEvents.isEmpty()) { + listener.onMouseMotionEvent(mouseMotionEvents.poll()); + } + + while (!mouseButtonEvents.isEmpty()) { + listener.onMouseButtonEvent(mouseButtonEvents.poll()); + } + } + + public void destroy() { + if (!context.isRenderable()) { + return; + } + + cursorPosCallback.release(); + scrollCallback.release(); + mouseButtonCallback.release(); + + logger.fine("Mouse destroyed."); + } + + public void setCursorVisible(boolean visible) { + cursorVisible = visible; + + if (!context.isRenderable()) { + return; + } + + if (cursorVisible) { + glfwSetInputMode(context.getWindowHandle(), GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } else { + glfwSetInputMode(context.getWindowHandle(), GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } + } + + public void setInputListener(RawInputListener listener) { + this.listener = listener; + } + + public long getInputTimeNanos() { + return (long) (glfwGetTime() * LwjglTimer.LWJGL_TIME_TO_NANOS); + } + + public void setNativeCursor(final JmeCursor jmeCursor) { + if (jmeCursor != null) { + final ByteBuffer byteBuffer = org.lwjgl.BufferUtils.createByteBuffer(jmeCursor.getImagesData().capacity()); + byteBuffer.asIntBuffer().put(jmeCursor.getImagesData().array()); + final long cursor = glfwCreateCursor(byteBuffer, jmeCursor.getXHotSpot(), jmeCursor.getYHotSpot()); + glfwSetCursor(context.getWindowHandle(), cursor); + } + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java new file mode 100644 index 000000000..80e7f3513 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java @@ -0,0 +1,458 @@ +package com.jme3.renderer.lwjgl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GL; +import com.jme3.renderer.opengl.GL2; +import com.jme3.renderer.opengl.GL3; +import com.jme3.renderer.opengl.GL4; +import com.jme3.system.NativeLibraryLoader; +import com.jme3.system.Platform; +import org.lwjgl.opengl.*; + +import java.nio.*; + +public class LwjglGL implements GL, GL2, GL3, GL4 { + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + public void resetStats() { + } + + public void glActiveTexture(int param1) { + GL13.glActiveTexture(param1); + } + + public void glAlphaFunc(int param1, float param2) { + GL11.glAlphaFunc(param1, param2); + } + + public void glAttachShader(int param1, int param2) { + GL20.glAttachShader(param1, param2); + } + + public void glBindBuffer(int param1, int param2) { + GL15.glBindBuffer(param1, param2); + } + + public void glBindTexture(int param1, int param2) { + GL11.glBindTexture(param1, param2); + } + + public void glBlendFunc(int param1, int param2) { + GL11.glBlendFunc(param1, param2); + } + + public void glBufferData(int param1, long param2, int param3) { + GL15.glBufferData(param1, param2, param3); + } + + public void glBufferData(int param1, FloatBuffer param2, int param3) { + checkLimit(param2); + GL15.glBufferData(param1, param2, param3); + } + + public void glBufferData(int param1, ShortBuffer param2, int param3) { + checkLimit(param2); + GL15.glBufferData(param1, param2, param3); + } + + public void glBufferData(int param1, ByteBuffer param2, int param3) { + checkLimit(param2); + GL15.glBufferData(param1, param2, param3); + } + + public void glBufferSubData(int param1, long param2, FloatBuffer param3) { + checkLimit(param3); + GL15.glBufferSubData(param1, param2, param3); + } + + public void glBufferSubData(int param1, long param2, ShortBuffer param3) { + checkLimit(param3); + GL15.glBufferSubData(param1, param2, param3); + } + + public void glBufferSubData(int param1, long param2, ByteBuffer param3) { + checkLimit(param3); + GL15.glBufferSubData(param1, param2, param3); + } + + public void glClear(int param1) { + GL11.glClear(param1); + } + + public void glClearColor(float param1, float param2, float param3, float param4) { + GL11.glClearColor(param1, param2, param3, param4); + } + + public void glColorMask(boolean param1, boolean param2, boolean param3, boolean param4) { + GL11.glColorMask(param1, param2, param3, param4); + } + + public void glCompileShader(int param1) { + GL20.glCompileShader(param1); + } + + public void glCompressedTexImage2D(int param1, int param2, int param3, int param4, int param5, int param6, ByteBuffer param7) { + checkLimit(param7); + GL13.glCompressedTexImage2D(param1, param2, param3, param4, param5, param6, param7); + } + + public void glCompressedTexImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, ByteBuffer param8) { + checkLimit(param8); + GL13.glCompressedTexImage3D(param1, param2, param3, param4, param5, param6, param7, param8); + } + + public void glCompressedTexSubImage2D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, ByteBuffer param8) { + checkLimit(param8); + GL13.glCompressedTexSubImage2D(param1, param2, param3, param4, param5, param6, param7, param8); + } + + public void glCompressedTexSubImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, int param9, ByteBuffer param10) { + checkLimit(param10); + GL13.glCompressedTexSubImage3D(param1, param2, param3, param4, param5, param6, param7, param8, param9, param10); + } + + public int glCreateProgram() { + return GL20.glCreateProgram(); + } + + public int glCreateShader(int param1) { + return GL20.glCreateShader(param1); + } + + public void glCullFace(int param1) { + GL11.glCullFace(param1); + } + + public void glDeleteBuffers(IntBuffer param1) { + checkLimit(param1); + GL15.glDeleteBuffers(param1); + } + + public void glDeleteProgram(int param1) { + GL20.glDeleteProgram(param1); + } + + public void glDeleteShader(int param1) { + GL20.glDeleteShader(param1); + } + + public void glDeleteTextures(IntBuffer param1) { + checkLimit(param1); + GL11.glDeleteTextures(param1); + } + + public void glDepthFunc(int param1) { + GL11.glDepthFunc(param1); + } + + public void glDepthMask(boolean param1) { + GL11.glDepthMask(param1); + } + + public void glDepthRange(double param1, double param2) { + GL11.glDepthRange(param1, param2); + } + + public void glDetachShader(int param1, int param2) { + GL20.glDetachShader(param1, param2); + } + + public void glDisable(int param1) { + GL11.glDisable(param1); + } + + public void glDisableVertexAttribArray(int param1) { + GL20.glDisableVertexAttribArray(param1); + } + + public void glDrawArrays(int param1, int param2, int param3) { + GL11.glDrawArrays(param1, param2, param3); + } + + public void glDrawBuffer(int param1) { + GL11.glDrawBuffer(param1); + } + + public void glDrawRangeElements(int param1, int param2, int param3, int param4, int param5, long param6) { + GL12.glDrawRangeElements(param1, param2, param3, param4, param5, param6); + } + + public void glEnable(int param1) { + GL11.glEnable(param1); + } + + public void glEnableVertexAttribArray(int param1) { + GL20.glEnableVertexAttribArray(param1); + } + + public void glGenBuffers(IntBuffer param1) { + checkLimit(param1); + GL15.glGenBuffers(param1); + } + + public void glGenTextures(IntBuffer param1) { + checkLimit(param1); + GL11.glGenTextures(param1); + } + + public void glGetBoolean(int param1, ByteBuffer param2) { + checkLimit(param2); + GL11.glGetBooleanv(param1, param2); + } + + public void glGetBufferSubData(int target, long offset, ByteBuffer data) { + checkLimit(data); + GL15.glGetBufferSubData(target, offset, data); + } + + public int glGetError() { + return GL11.glGetError(); + } + + public void glGetInteger(int param1, IntBuffer param2) { + checkLimit(param2); + GL11.glGetIntegerv(param1, param2); + } + + public void glGetProgram(int param1, int param2, IntBuffer param3) { + checkLimit(param3); + GL20.glGetProgramiv(param1, param2, param3); + } + + public void glGetShader(int param1, int param2, IntBuffer param3) { + checkLimit(param3); + GL20.glGetShaderiv(param1, param2, param3); + } + + public String glGetString(int param1) { + return GL11.glGetString(param1); + } + + public String glGetString(int param1, int param2) { + return GL30.glGetStringi(param1, param2); + } + + public boolean glIsEnabled(int param1) { + return GL11.glIsEnabled(param1); + } + + public void glLineWidth(float param1) { + GL11.glLineWidth(param1); + } + + public void glLinkProgram(int param1) { + GL20.glLinkProgram(param1); + } + + public void glPixelStorei(int param1, int param2) { + GL11.glPixelStorei(param1, param2); + } + + public void glPointSize(float param1) { + GL11.glPointSize(param1); + } + + public void glPolygonMode(int param1, int param2) { + GL11.glPolygonMode(param1, param2); + } + + public void glPolygonOffset(float param1, float param2) { + GL11.glPolygonOffset(param1, param2); + } + + public void glReadBuffer(int param1) { + GL11.glReadBuffer(param1); + } + + public void glReadPixels(int param1, int param2, int param3, int param4, int param5, int param6, ByteBuffer param7) { + checkLimit(param7); + GL11.glReadPixels(param1, param2, param3, param4, param5, param6, param7); + } + + public void glReadPixels(int param1, int param2, int param3, int param4, int param5, int param6, long param7) { + GL11.glReadPixels(param1, param2, param3, param4, param5, param6, param7); + } + + public void glScissor(int param1, int param2, int param3, int param4) { + GL11.glScissor(param1, param2, param3, param4); + } + + public void glStencilFuncSeparate(int param1, int param2, int param3, int param4) { + GL20.glStencilFuncSeparate(param1, param2, param3, param4); + } + + public void glStencilOpSeparate(int param1, int param2, int param3, int param4) { + GL20.glStencilOpSeparate(param1, param2, param3, param4); + } + + public void glTexImage2D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, ByteBuffer param9) { + checkLimit(param9); + GL11.glTexImage2D(param1, param2, param3, param4, param5, param6, param7, param8, param9); + } + + public void glTexImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, int param9, ByteBuffer param10) { + checkLimit(param10); + GL12.glTexImage3D(param1, param2, param3, param4, param5, param6, param7, param8, param9, param10); + } + + public void glTexParameterf(int param1, int param2, float param3) { + GL11.glTexParameterf(param1, param2, param3); + } + + public void glTexParameteri(int param1, int param2, int param3) { + GL11.glTexParameteri(param1, param2, param3); + } + + public void glTexSubImage2D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, ByteBuffer param9) { + checkLimit(param9); + GL11.glTexSubImage2D(param1, param2, param3, param4, param5, param6, param7, param8, param9); + } + + public void glTexSubImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, int param9, int param10, ByteBuffer param11) { + checkLimit(param11); + GL12.glTexSubImage3D(param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11); + } + + public void glUniform1(int param1, FloatBuffer param2) { + checkLimit(param2); + GL20.glUniform1fv(param1, param2); + } + + public void glUniform1(int param1, IntBuffer param2) { + checkLimit(param2); + GL20.glUniform1iv(param1, param2); + } + + public void glUniform1f(int param1, float param2) { + GL20.glUniform1f(param1, param2); + } + + public void glUniform1i(int param1, int param2) { + GL20.glUniform1i(param1, param2); + } + + public void glUniform2(int param1, IntBuffer param2) { + checkLimit(param2); + GL20.glUniform2iv(param1, param2); + } + + public void glUniform2(int param1, FloatBuffer param2) { + checkLimit(param2); + GL20.glUniform2fv(param1, param2); + } + + public void glUniform2f(int param1, float param2, float param3) { + GL20.glUniform2f(param1, param2, param3); + } + + public void glUniform3(int param1, IntBuffer param2) { + checkLimit(param2); + GL20.glUniform3iv(param1, param2); + } + + public void glUniform3(int param1, FloatBuffer param2) { + checkLimit(param2); + GL20.glUniform3fv(param1, param2); + } + + public void glUniform3f(int param1, float param2, float param3, float param4) { + GL20.glUniform3f(param1, param2, param3, param4); + } + + public void glUniform4(int param1, FloatBuffer param2) { + checkLimit(param2); + GL20.glUniform4fv(param1, param2); + } + + public void glUniform4(int param1, IntBuffer param2) { + checkLimit(param2); + GL20.glUniform4iv(param1, param2); + } + + public void glUniform4f(int param1, float param2, float param3, float param4, float param5) { + GL20.glUniform4f(param1, param2, param3, param4, param5); + } + + public void glUniformMatrix3(int param1, boolean param2, FloatBuffer param3) { + checkLimit(param3); + GL20.glUniformMatrix3fv(param1, param2, param3); + } + + public void glUniformMatrix4(int param1, boolean param2, FloatBuffer param3) { + checkLimit(param3); + GL20.glUniformMatrix4fv(param1, param2, param3); + } + + public void glUseProgram(int param1) { + GL20.glUseProgram(param1); + } + + public void glVertexAttribPointer(int param1, int param2, int param3, boolean param4, int param5, long param6) { + GL20.glVertexAttribPointer(param1, param2, param3, param4, param5, param6); + } + + public void glViewport(int param1, int param2, int param3, int param4) { + GL11.glViewport(param1, param2, param3, param4); + } + + public int glGetAttribLocation(int param1, String param2) { + // NOTE: LWJGL requires null-terminated strings + return GL20.glGetAttribLocation(param1, param2 + "\0"); + } + + public int glGetUniformLocation(int param1, String param2) { + // NOTE: LWJGL requires null-terminated strings + return GL20.glGetUniformLocation(param1, param2 + "\0"); + } + + public void glShaderSource(int param1, String[] param2, IntBuffer param3) { + checkLimit(param3); + GL20.glShaderSource(param1, param2); + } + + public String glGetProgramInfoLog(int program, int maxSize) { + return GL20.glGetProgramInfoLog(program, maxSize); + } + + public String glGetShaderInfoLog(int shader, int maxSize) { + return GL20.glGetShaderInfoLog(shader, maxSize); + } + + @Override + public void glBindFragDataLocation(int param1, int param2, String param3) { + GL30.glBindFragDataLocation(param1, param2, param3); + } + + @Override + public void glBindVertexArray(int param1) { + GL30.glBindVertexArray(param1); + } + + @Override + public void glGenVertexArrays(IntBuffer param1) { + checkLimit(param1); + GL30.glGenVertexArrays(param1); + } + + @Override + public void glPatchParameter(int count) { + GL40.glPatchParameteri(GL40.GL_PATCH_VERTICES,count); + } + + @Override + public void glDeleteVertexArrays(IntBuffer arrays) { + checkLimit(arrays); + ARBVertexArrayObject.glDeleteVertexArrays(arrays); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java new file mode 100644 index 000000000..00b3f688c --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -0,0 +1,83 @@ +package com.jme3.renderer.lwjgl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GLExt; +import org.lwjgl.opengl.*; + +import java.nio.Buffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public class LwjglGLExt implements GLExt { + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + @Override + public void glBufferData(int target, IntBuffer data, int usage) { + checkLimit(data); + GL15.glBufferData(target, data, usage); + } + + @Override + public void glBufferSubData(int target, long offset, IntBuffer data) { + checkLimit(data); + GL15.glBufferSubData(target, offset, data); + } + + @Override + public void glDrawArraysInstancedARB(int mode, int first, int count, int primcount) { + ARBDrawInstanced.glDrawArraysInstancedARB(mode, first, count, primcount); + } + + @Override + public void glDrawBuffers(IntBuffer bufs) { + checkLimit(bufs); + GL20.glDrawBuffers(bufs); + } + + @Override + public void glDrawElementsInstancedARB(int mode, int indices_count, int type, long indices_buffer_offset, int primcount) { + ARBDrawInstanced.glDrawElementsInstancedARB(mode, indices_count, type, indices_buffer_offset, primcount); + } + + @Override + public void glGetMultisample(int pname, int index, FloatBuffer val) { + checkLimit(val); + ARBTextureMultisample.glGetMultisamplefv(pname, index, val); + } + + @Override + public void glTexImage2DMultisample(int target, int samples, int internalformat, int width, int height, boolean fixedsamplelocations) { + ARBTextureMultisample.glTexImage2DMultisample(target, samples, internalformat, width, height, fixedsamplelocations); + } + + @Override + public void glVertexAttribDivisorARB(int index, int divisor) { + ARBInstancedArrays.glVertexAttribDivisorARB(index, divisor); + } + + @Override + public Object glFenceSync(int condition, int flags) { + return ARBSync.glFenceSync(condition, flags); + } + + @Override + public int glClientWaitSync(final Object sync, final int flags, final long timeout) { + return ARBSync.glClientWaitSync((Long) sync, flags, timeout); + } + + @Override + public void glDeleteSync(final Object sync) { + ARBSync.glDeleteSync((Long) sync); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java new file mode 100644 index 000000000..969d7ae1d --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java @@ -0,0 +1,99 @@ +package com.jme3.renderer.lwjgl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GLFbo; +import org.lwjgl.opengl.EXTFramebufferBlit; +import org.lwjgl.opengl.EXTFramebufferMultisample; +import org.lwjgl.opengl.EXTFramebufferObject; + +import java.nio.Buffer; +import java.nio.IntBuffer; + +/** + * Implements GLFbo via GL_EXT_framebuffer_object. + * + * @author Kirill Vainer + */ +public class LwjglGLFboEXT implements GLFbo { + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + @Override + public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) { + EXTFramebufferBlit.glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } + + @Override + public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height) { + EXTFramebufferMultisample.glRenderbufferStorageMultisampleEXT(target, samples, internalformat, width, height); + } + + @Override + public void glBindFramebufferEXT(int param1, int param2) { + EXTFramebufferObject.glBindFramebufferEXT(param1, param2); + } + + @Override + public void glBindRenderbufferEXT(int param1, int param2) { + EXTFramebufferObject.glBindRenderbufferEXT(param1, param2); + } + + @Override + public int glCheckFramebufferStatusEXT(int param1) { + return EXTFramebufferObject.glCheckFramebufferStatusEXT(param1); + } + + @Override + public void glDeleteFramebuffersEXT(IntBuffer param1) { + checkLimit(param1); + EXTFramebufferObject.glDeleteFramebuffersEXT(param1); + } + + @Override + public void glDeleteRenderbuffersEXT(IntBuffer param1) { + checkLimit(param1); + EXTFramebufferObject.glDeleteRenderbuffersEXT(param1); + } + + @Override + public void glFramebufferRenderbufferEXT(int param1, int param2, int param3, int param4) { + EXTFramebufferObject.glFramebufferRenderbufferEXT(param1, param2, param3, param4); + } + + @Override + public void glFramebufferTexture2DEXT(int param1, int param2, int param3, int param4, int param5) { + EXTFramebufferObject.glFramebufferTexture2DEXT(param1, param2, param3, param4, param5); + } + + @Override + public void glGenFramebuffersEXT(IntBuffer param1) { + checkLimit(param1); + EXTFramebufferObject.glGenFramebuffersEXT(param1); + } + + @Override + public void glGenRenderbuffersEXT(IntBuffer param1) { + checkLimit(param1); + EXTFramebufferObject.glGenRenderbuffersEXT(param1); + } + + @Override + public void glGenerateMipmapEXT(int param1) { + EXTFramebufferObject.glGenerateMipmapEXT(param1); + } + + @Override + public void glRenderbufferStorageEXT(int param1, int param2, int param3, int param4) { + EXTFramebufferObject.glRenderbufferStorageEXT(param1, param2, param3, param4); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java new file mode 100644 index 000000000..aa15aeb09 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java @@ -0,0 +1,97 @@ +package com.jme3.renderer.lwjgl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GLFbo; +import org.lwjgl.opengl.GL30; + +import java.nio.Buffer; +import java.nio.IntBuffer; + +/** + * Implements GLFbo via OpenGL3+. + * + * @author Kirill Vainer + */ +public class LwjglGLFboGL3 implements GLFbo { + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + @Override + public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) { + GL30.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } + + @Override + public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height) { + GL30.glRenderbufferStorageMultisample(target, samples, internalformat, width, height); + } + + @Override + public void glBindFramebufferEXT(int param1, int param2) { + GL30.glBindFramebuffer(param1, param2); + } + + @Override + public void glBindRenderbufferEXT(int param1, int param2) { + GL30.glBindRenderbuffer(param1, param2); + } + + @Override + public int glCheckFramebufferStatusEXT(int param1) { + return GL30.glCheckFramebufferStatus(param1); + } + + @Override + public void glDeleteFramebuffersEXT(IntBuffer param1) { + checkLimit(param1); + GL30.glDeleteFramebuffers(param1); + } + + @Override + public void glDeleteRenderbuffersEXT(IntBuffer param1) { + checkLimit(param1); + GL30.glDeleteRenderbuffers(param1); + } + + @Override + public void glFramebufferRenderbufferEXT(int param1, int param2, int param3, int param4) { + GL30.glFramebufferRenderbuffer(param1, param2, param3, param4); + } + + @Override + public void glFramebufferTexture2DEXT(int param1, int param2, int param3, int param4, int param5) { + GL30.glFramebufferTexture2D(param1, param2, param3, param4, param5); + } + + @Override + public void glGenFramebuffersEXT(IntBuffer param1) { + checkLimit(param1); + GL30.glGenFramebuffers(param1); + } + + @Override + public void glGenRenderbuffersEXT(IntBuffer param1) { + checkLimit(param1); + GL30.glGenRenderbuffers(param1); + } + + @Override + public void glGenerateMipmapEXT(int param1) { + GL30.glGenerateMipmap(param1); + } + + @Override + public void glRenderbufferStorageEXT(int param1, int param2, int param3, int param4) { + GL30.glRenderbufferStorage(param1, param2, param3, param4); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java new file mode 100644 index 000000000..1f1ef2ab5 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.system.lwjgl; + +import com.jme3.system.AppSettings; +import com.jme3.system.JmeCanvasContext; + +import javax.swing.*; +import java.awt.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.glfwDestroyWindow; + +public class LwjglCanvas extends LwjglWindow implements JmeCanvasContext { + + protected static final int TASK_NOTHING = 0, + TASK_DESTROY_DISPLAY = 1, + TASK_CREATE_DISPLAY = 2, + TASK_COMPLETE = 3; + +// protected static final boolean USE_SHARED_CONTEXT = +// Boolean.parseBoolean(System.getProperty("jme3.canvas.sharedctx", "true")); + + protected static final boolean USE_SHARED_CONTEXT = false; + + private static final Logger logger = Logger.getLogger(LwjglCanvas.class.getName()); + private Canvas canvas; + private int width; + private int height; + + private final Object taskLock = new Object(); + private int desiredTask = TASK_NOTHING; + + private Thread renderThread; + private boolean runningFirstTime = true; + private boolean mouseWasGrabbed = false; + + private boolean mouseWasCreated = false; + private boolean keyboardWasCreated = false; + + private long window; + + private class GLCanvas extends Canvas { + @Override + public void addNotify(){ + super.addNotify(); + + if (renderThread != null && renderThread.getState() == Thread.State.TERMINATED) { + return; // already destroyed. + } + + if (renderThread == null){ + logger.log(Level.FINE, "EDT: Creating OGL thread."); + + // Also set some settings on the canvas here. + // So we don't do it outside the AWT thread. + canvas.setFocusable(true); + canvas.setIgnoreRepaint(true); + + renderThread = new Thread(LwjglCanvas.this, THREAD_NAME); + renderThread.start(); + }else if (needClose.get()){ + return; + } + + logger.log(Level.FINE, "EDT: Telling OGL to create display .."); + synchronized (taskLock){ + desiredTask = TASK_CREATE_DISPLAY; +// while (desiredTask != TASK_COMPLETE){ +// try { +// taskLock.wait(); +// } catch (InterruptedException ex) { +// return; +// } +// } +// desiredTask = TASK_NOTHING; + } +// logger.log(Level.FINE, "EDT: OGL has created the display"); + } + + @Override + public void removeNotify(){ + if (needClose.get()){ + logger.log(Level.FINE, "EDT: Application is stopped. Not restoring canvas."); + super.removeNotify(); + return; + } + + // We must tell GL context to shutdown and wait for it to + // shutdown, otherwise, issues will occur. + logger.log(Level.FINE, "EDT: Telling OGL to destroy display .."); + synchronized (taskLock){ + desiredTask = TASK_DESTROY_DISPLAY; + while (desiredTask != TASK_COMPLETE){ + try { + taskLock.wait(); + } catch (InterruptedException ex){ + super.removeNotify(); + return; + } + } + desiredTask = TASK_NOTHING; + } + + logger.log(Level.FINE, "EDT: Acknowledged receipt of canvas death"); + // GL context is dead at this point + + super.removeNotify(); + } + } + + public LwjglCanvas(){ + super(Type.Canvas); + canvas = new GLCanvas(); + } + + public void create(boolean waitFor){ + if (renderThread == null){ + logger.log(Level.FINE, "MAIN: Creating OGL thread."); + + renderThread = new Thread(LwjglCanvas.this, THREAD_NAME); + renderThread.start(); + } + // do not do anything. + // superclass's create() will be called at initInThread() + if (waitFor) { + waitFor(true); + } + } + + public Canvas getCanvas(){ + return canvas; + } + + @Override + protected void runLoop(){ + if (desiredTask != TASK_NOTHING){ + synchronized (taskLock){ + switch (desiredTask){ + case TASK_CREATE_DISPLAY: + logger.log(Level.FINE, "OGL: Creating display .."); + restoreCanvas(); + listener.gainFocus(); + desiredTask = TASK_NOTHING; + break; + case TASK_DESTROY_DISPLAY: + logger.log(Level.FINE, "OGL: Destroying display .."); + listener.loseFocus(); + pauseCanvas(); + break; + } + desiredTask = TASK_COMPLETE; + taskLock.notifyAll(); + } + } + + if (renderable.get()){ + int newWidth = Math.max(canvas.getWidth(), 1); + int newHeight = Math.max(canvas.getHeight(), 1); + + if (width != newWidth || height != newHeight){ + width = newWidth; + height = newHeight; + if (listener != null){ + listener.reshape(width, height); + } + } + } + + super.runLoop(); + } + + private void pauseCanvas(){ + if (mouseInput != null) { + mouseInput.setCursorVisible(true); + mouseWasCreated = true; + } + +/* + if (Mouse.isCreated()){ + if (Mouse.isGrabbed()){ + Mouse.setGrabbed(false); + mouseWasGrabbed = true; + } + mouseWasCreated = true; + Mouse.destroy(); + } + if (Keyboard.isCreated()){ + keyboardWasCreated = true; + Keyboard.destroy(); + } +*/ + + renderable.set(false); + destroyContext(); + } + + /** + * Called to restore the canvas. + */ + private void restoreCanvas(){ + logger.log(Level.FINE, "OGL: Waiting for canvas to become displayable.."); + while (!canvas.isDisplayable()){ + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + logger.log(Level.SEVERE, "OGL: Interrupted! ", ex); + } + } + + logger.log(Level.FINE, "OGL: Creating display context .."); + + // Set renderable to true, since canvas is now displayable. + renderable.set(true); + createContext(settings); + + logger.log(Level.FINE, "OGL: Display is active!"); + + try { + if (mouseWasCreated){ +// Mouse.create(); +// if (mouseWasGrabbed){ +// Mouse.setGrabbed(true); +// mouseWasGrabbed = false; +// } + } + if (keyboardWasCreated){ +// Keyboard.create(); +// keyboardWasCreated = false; + } + } catch (Exception ex){ + logger.log(Level.SEVERE, "Encountered exception when restoring input", ex); + } + + SwingUtilities.invokeLater(new Runnable(){ + public void run(){ + canvas.requestFocus(); + } + }); + } + +/* + */ +/** + * Makes sure the pbuffer is available and ready for use + *//* + + protected void makePbufferAvailable() throws LWJGLException{ + if (pbuffer != null && pbuffer.isBufferLost()){ + logger.log(Level.WARNING, "PBuffer was lost!"); + pbuffer.destroy(); + pbuffer = null; + } + + if (pbuffer == null) { + pbuffer = new Pbuffer(1, 1, acquirePixelFormat(true), null); + pbuffer.makeCurrent(); + logger.log(Level.FINE, "OGL: Pbuffer has been created"); + + // Any created objects are no longer valid + if (!runningFirstTime){ + renderer.resetGLObjects(); + } + } + + pbuffer.makeCurrent(); + if (!pbuffer.isCurrent()){ + throw new LWJGLException("Pbuffer cannot be made current"); + } + } + + protected void destroyPbuffer(){ + if (pbuffer != null){ + if (!pbuffer.isBufferLost()){ + pbuffer.destroy(); + } + pbuffer = null; + } + } +*/ + + /** + * This is called: + * 1) When the context thread ends + * 2) Any time the canvas becomes non-displayable + */ + protected void destroyContext(){ + // invalidate the state so renderer can resume operation + if (!USE_SHARED_CONTEXT){ + renderer.cleanup(); + } + + if (window != 0) { + glfwDestroyWindow(window); + } + + // TODO: Destroy input + + + // The canvas is no longer visible, + // but the context thread is still running. + if (!needClose.get()){ + renderer.invalidateState(); + } + } + + /** + * This is called: + * 1) When the context thread starts + * 2) Any time the canvas becomes displayable again. + */ + @Override + protected void createContext(final AppSettings settings) { + // In case canvas is not visible, we still take framerate + // from settings to prevent "100% CPU usage" + allowSwapBuffers = settings.isSwapBuffers(); + + if (renderable.get()){ + if (!runningFirstTime){ + // because the display is a different opengl context + // must reset the context state. + if (!USE_SHARED_CONTEXT){ + renderer.cleanup(); + } + } + + super.createContext(settings); + } + + // At this point, the OpenGL context is active. + if (runningFirstTime) { + // THIS is the part that creates the renderer. + // It must always be called, now that we have the pbuffer workaround. + initContextFirstTime(); + runningFirstTime = false; + } + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java new file mode 100644 index 000000000..3f6491e5f --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.system.lwjgl; + +import com.jme3.input.lwjgl.GlfwJoystickInput; +import com.jme3.input.lwjgl.LwjglKeyInput; +import com.jme3.input.lwjgl.LwjglMouseInput; +import com.jme3.renderer.Renderer; +import com.jme3.renderer.RendererException; +import com.jme3.renderer.lwjgl.LwjglGL; +import com.jme3.renderer.lwjgl.LwjglGLExt; +import com.jme3.renderer.lwjgl.LwjglGLFboEXT; +import com.jme3.renderer.lwjgl.LwjglGLFboGL3; +import com.jme3.renderer.opengl.*; +import com.jme3.renderer.opengl.GL; +import com.jme3.system.*; +import org.lwjgl.Sys; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.*; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.lwjgl.opengl.GL.createCapabilities; +import static org.lwjgl.opengl.GL11.GL_TRUE; +import static org.lwjgl.opengl.GL11.glGetInteger; + +/** + * A LWJGL implementation of a graphics context. + */ +public abstract class LwjglContext implements JmeContext { + + private static final Logger logger = Logger.getLogger(LwjglContext.class.getName()); + + protected static final String THREAD_NAME = "jME3 Main"; + + protected AtomicBoolean created = new AtomicBoolean(false); + protected AtomicBoolean renderable = new AtomicBoolean(false); + protected final Object createdLock = new Object(); + + protected AppSettings settings = new AppSettings(true); + protected Renderer renderer; + protected LwjglKeyInput keyInput; + protected LwjglMouseInput mouseInput; + protected GlfwJoystickInput joyInput; + protected Timer timer; + protected SystemListener listener; + + public void setSystemListener(SystemListener listener) { + this.listener = listener; + } + + protected void printContextInitInfo() { + logger.log(Level.INFO, "LWJGL {0} context running on thread {1}\n" + + " * Graphics Adapter: GLFW {2}", + new Object[]{Sys.getVersion(), Thread.currentThread().getName(), GLFW.glfwGetVersionString()}); + } + + protected int determineMaxSamples() { + // If we already have a valid context, determine samples using current context. + if (GLFW.glfwExtensionSupported("GL_ARB_framebuffer_object") == GL_TRUE) { + return glGetInteger(ARBFramebufferObject.GL_MAX_SAMPLES); + } else if (GLFW.glfwExtensionSupported("GL_EXT_framebuffer_multisample") == GL_TRUE) { + return glGetInteger(EXTFramebufferMultisample.GL_MAX_SAMPLES_EXT); + } + + return Integer.MAX_VALUE; + } + + protected void registerNatives() { + // LWJGL + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/lwjgl32.dll"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/lwjgl.dll"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux32, "native/linux/liblwjgl32.so"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux64, "native/linux/liblwjgl.so"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/jemalloc32.dll"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/jemalloc.dll"); + + // OpenAL + // For OSX: Need to add lib prefix when extracting + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows32, "native/windows/OpenAL32.dll"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows64, "native/windows/OpenAL.dll"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux32, "native/linux/libopenal32.so"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux64, "native/linux/libopenal.so"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX32, "native/macosx/openal.dylib", "libopenal.dylib"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX64, "native/macosx/openal.dylib", "libopenal.dylib"); + } + + protected void loadNatives() { + if (JmeSystem.isLowPermissions()) { + return; + } + if ("LWJGL".equals(settings.getAudioRenderer())) { + NativeLibraryLoader.loadNativeLibrary("openal", true); + } + if (settings.useJoysticks()) { + //NativeLibraryLoader.loadNativeLibrary("jinput", true); + //NativeLibraryLoader.loadNativeLibrary("jinput-dx8", true); + } + if (NativeLibraryLoader.isUsingNativeBullet()) { + NativeLibraryLoader.loadNativeLibrary("bulletjme", true); + } + NativeLibraryLoader.loadNativeLibrary("lwjgl", true); + } + + protected int getNumSamplesToUse() { + int samples = 0; + if (settings.getSamples() > 1) { + samples = settings.getSamples(); + final int supportedSamples = determineMaxSamples(); + if (supportedSamples < samples) { + logger.log(Level.WARNING, + "Couldn't satisfy antialiasing samples requirement: x{0}. " + + "Video hardware only supports: x{1}", + new Object[]{samples, supportedSamples}); + + samples = supportedSamples; + } + } + return samples; + } + + protected void initContextFirstTime() { + final GLCapabilities capabilities = createCapabilities(settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)); + + if (!capabilities.OpenGL20) { + throw new RendererException("OpenGL 2.0 or higher is required for jMonkeyEngine"); + } + + if (settings.getRenderer().equals(AppSettings.LWJGL_OPENGL2) + || settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)) { + GL gl = new LwjglGL(); + GLExt glext = new LwjglGLExt(); + GLFbo glfbo; + + if (capabilities.OpenGL30) { + glfbo = new LwjglGLFboGL3(); + } else { + glfbo = new LwjglGLFboEXT(); + } + + if (settings.getBoolean("GraphicsDebug")) { + gl = new GLDebugDesktop(gl, glext, glfbo); + glext = (GLExt) gl; + glfbo = (GLFbo) gl; + } + + if (settings.getBoolean("GraphicsTiming")) { + GLTimingState timingState = new GLTimingState(); + gl = (GL) GLTiming.createGLTiming(gl, timingState, GL.class, GL2.class, GL3.class, GL4.class); + glext = (GLExt) GLTiming.createGLTiming(glext, timingState, GLExt.class); + glfbo = (GLFbo) GLTiming.createGLTiming(glfbo, timingState, GLFbo.class); + } + + if (settings.getBoolean("GraphicsTrace")) { + gl = (GL) GLTracer.createDesktopGlTracer(gl, GL.class, GL2.class, GL3.class, GL4.class); + glext = (GLExt) GLTracer.createDesktopGlTracer(glext, GLExt.class); + glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLFbo.class); + } + + renderer = new GLRenderer(gl, glext, glfbo); + renderer.initialize(); + } else { + throw new UnsupportedOperationException("Unsupported renderer: " + settings.getRenderer()); + } + + if (capabilities.GL_ARB_debug_output && settings.getBoolean("GraphicsDebug")) { + ARBDebugOutput.glDebugMessageCallbackARB(new LwjglGLDebugOutputHandler(), 0); // User param is zero. Not sure what we could use that for. + } + + renderer.setMainFrameBufferSrgb(settings.getGammaCorrection()); + renderer.setLinearizeSrgbImages(settings.getGammaCorrection()); + + // Init input + if (keyInput != null) { + keyInput.initialize(); + } + + if (mouseInput != null) { + mouseInput.initialize(); + } + + if (joyInput != null) { + joyInput.initialize(); + } + + renderable.set(true); + } + + public void internalDestroy() { + renderer = null; + timer = null; + renderable.set(false); + synchronized (createdLock) { + created.set(false); + createdLock.notifyAll(); + } + } + + public void internalCreate() { + synchronized (createdLock) { + created.set(true); + createdLock.notifyAll(); + } + + //if (renderable.get()) { + initContextFirstTime(); + //} else { +// assert getType() == Type.Canvas; +// } + } + + public void create() { + create(false); + } + + public void destroy() { + destroy(false); + } + + protected void waitFor(boolean createdVal) { + synchronized (createdLock) { + while (created.get() != createdVal) { + try { + createdLock.wait(); + } catch (InterruptedException ex) { + } + } + } + } + + public boolean isCreated() { + return created.get(); + } + + public boolean isRenderable() { + return renderable.get(); + } + + public void setSettings(AppSettings settings) { + this.settings.copyFrom(settings); + } + + public AppSettings getSettings() { + return settings; + } + + public Renderer getRenderer() { + return renderer; + } + + public Timer getTimer() { + return timer; + } + +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java new file mode 100644 index 000000000..4a13c30af --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java @@ -0,0 +1,12 @@ +package com.jme3.system.lwjgl; + +/** + * @author Daniel Johansson + * @since 2015-08-11 + */ +public class LwjglDisplay extends LwjglWindow { + + public LwjglDisplay() { + super(Type.Display); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java new file mode 100644 index 000000000..aa9c46511 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2009-2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.system.lwjgl; + +import org.lwjgl.opengl.ARBDebugOutput; +import org.lwjgl.opengl.GLDebugMessageARBCallback; + +import java.util.HashMap; + +class LwjglGLDebugOutputHandler extends GLDebugMessageARBCallback { + + private static final HashMap constMap = new HashMap(); + private static final String MESSAGE_FORMAT = + "[JME3] OpenGL debug message\r\n" + + " ID: %d\r\n" + + " Source: %s\r\n" + + " Type: %s\r\n" + + " Severity: %s\r\n" + + " Message: %s"; + + static { + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_API_ARB, "API"); + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_APPLICATION_ARB, "APPLICATION"); + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_OTHER_ARB, "OTHER"); + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_SHADER_COMPILER_ARB, "SHADER_COMPILER"); + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_THIRD_PARTY_ARB, "THIRD_PARTY"); + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_WINDOW_SYSTEM_ARB, "WINDOW_SYSTEM"); + + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_ARB, "DEPRECATED_BEHAVIOR"); + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_ERROR_ARB, "ERROR"); + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_OTHER_ARB, "OTHER"); + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_PERFORMANCE_ARB, "PERFORMANCE"); + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_PORTABILITY_ARB, "PORTABILITY"); + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_ARB, "UNDEFINED_BEHAVIOR"); + + constMap.put(ARBDebugOutput.GL_DEBUG_SEVERITY_HIGH_ARB, "HIGH"); + constMap.put(ARBDebugOutput.GL_DEBUG_SEVERITY_MEDIUM_ARB, "MEDIUM"); + constMap.put(ARBDebugOutput.GL_DEBUG_SEVERITY_LOW_ARB, "LOW"); + } + + @Override + public void invoke(int source, int type, int id, int severity, int length, long message, long userParam) { + String sourceStr = constMap.get(source); + String typeStr = constMap.get(type); + String severityStr = constMap.get(severity); + + System.err.println(String.format(MESSAGE_FORMAT, id, sourceStr, typeStr, severityStr, message)); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java new file mode 100644 index 000000000..e374e1841 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java @@ -0,0 +1,14 @@ +package com.jme3.system.lwjgl; + +import com.jme3.system.JmeContext; + +/** + * @author Daniel Johansson + * @since 2015-08-11 + */ +public class LwjglOffscreenBuffer extends LwjglWindow { + + public LwjglOffscreenBuffer() { + super(JmeContext.Type.OffscreenSurface); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java new file mode 100644 index 000000000..a7960a261 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.system.lwjgl; + +import com.jme3.math.FastMath; +import com.jme3.system.Timer; +import org.lwjgl.glfw.GLFW; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Timer handles the system's time related functionality. This + * allows the calculation of the framerate. To keep the framerate calculation + * accurate, a call to update each frame is required. Timer is a + * singleton object and must be created via the getTimer method. + * + * @author Mark Powell + * @version $Id: LWJGLTimer.java,v 1.21 2007/09/22 16:46:35 irrisor Exp $ + */ +public class LwjglSmoothingTimer extends LwjglTimer { + private static final Logger logger = Logger.getLogger(LwjglSmoothingTimer.class + .getName()); + + private long lastFrameDiff; + + //frame rate parameters. + private long oldTime; + + private float lastTPF, lastFPS; + + public static int TIMER_SMOOTHNESS = 32; + + private long[] tpf; + + private int smoothIndex; + + private final static long LWJGL_TIMER_RES = 1; + private final static float INV_LWJGL_TIMER_RES = ( 1f / LWJGL_TIMER_RES ); + private static float invTimerRezSmooth; + + public final static long LWJGL_TIME_TO_NANOS = (1000000000 / LWJGL_TIMER_RES); + + private long startTime; + + private boolean allSmooth = false; + + /** + * Constructor builds a Timer object. All values will be + * initialized to it's default values. + */ + public LwjglSmoothingTimer() { + reset(); + + //print timer resolution info + logger.log(Level.FINE, "Timer resolution: {0} ticks per second", LWJGL_TIMER_RES); + } + + public void reset() { + lastFrameDiff = 0; + lastFPS = 0; + lastTPF = 0; + + // init to -1 to indicate this is a new timer. + oldTime = -1; + //reset time + startTime = (long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS); + + tpf = new long[TIMER_SMOOTHNESS]; + smoothIndex = TIMER_SMOOTHNESS - 1; + invTimerRezSmooth = ( 1f / (LWJGL_TIMER_RES * TIMER_SMOOTHNESS)); + + // set tpf... -1 values will not be used for calculating the average in update() + for ( int i = tpf.length; --i >= 0; ) { + tpf[i] = -1; + } + } + + /** + * @see Timer#getResolution() + */ + public long getResolution() { + return LWJGL_TIMER_RES; + } + + /** + * getFrameRate returns the current frame rate since the last + * call to update. + * + * @return the current frame rate. + */ + public float getFrameRate() { + return lastFPS; + } + + public float getTimePerFrame() { + return lastTPF; + } + + /** + * update recalulates the frame rate based on the previous + * call to update. It is assumed that update is called each frame. + */ + public void update() { + long newTime = (long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS); + long oldTime = this.oldTime; + this.oldTime = newTime; + if ( oldTime == -1 ) { + // For the first frame use 60 fps. This value will not be counted in further averages. + // This is done so initialization code between creating the timer and the first + // frame is not counted as a single frame on it's own. + lastTPF = 1 / 60f; + lastFPS = 1f / lastTPF; + return; + } + + long frameDiff = newTime - oldTime; + long lastFrameDiff = this.lastFrameDiff; + if ( lastFrameDiff > 0 && frameDiff > lastFrameDiff *100 ) { + frameDiff = lastFrameDiff *100; + } + this.lastFrameDiff = frameDiff; + tpf[smoothIndex] = frameDiff; + smoothIndex--; + if ( smoothIndex < 0 ) { + smoothIndex = tpf.length - 1; + } + + lastTPF = 0.0f; + if (!allSmooth) { + int smoothCount = 0; + for ( int i = tpf.length; --i >= 0; ) { + if ( tpf[i] != -1 ) { + lastTPF += tpf[i]; + smoothCount++; + } + } + if (smoothCount == tpf.length) + allSmooth = true; + lastTPF *= ( INV_LWJGL_TIMER_RES / smoothCount ); + } else { + for ( int i = tpf.length; --i >= 0; ) { + if ( tpf[i] != -1 ) { + lastTPF += tpf[i]; + } + } + lastTPF *= invTimerRezSmooth; + } + if ( lastTPF < FastMath.FLT_EPSILON ) { + lastTPF = FastMath.FLT_EPSILON; + } + + lastFPS = 1f / lastTPF; + } + + /** + * toString returns the string representation of this timer + * in the format:
+ *
+ * jme.utility.Timer@1db699b
+ * Time: {LONG}
+ * FPS: {LONG}
+ * + * @return the string representation of this object. + */ + @Override + public String toString() { + String string = super.toString(); + string += "\nTime: " + oldTime; + string += "\nFPS: " + getFrameRate(); + return string; + } +} \ No newline at end of file diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java new file mode 100644 index 000000000..c4a0e5025 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.system.lwjgl; + +import com.jme3.system.Timer; +import org.lwjgl.glfw.GLFW; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Timer handles the system's time related functionality. This + * allows the calculation of the framerate. To keep the framerate calculation + * accurate, a call to update each frame is required. Timer is a + * singleton object and must be created via the getTimer method. + * + * @author Mark Powell + * @version $Id: LWJGLTimer.java,v 1.21 2007/09/22 16:46:35 irrisor Exp $ + */ +public class LwjglTimer extends Timer { + + private static final Logger logger = Logger.getLogger(LwjglTimer.class.getName()); + + //frame rate parameters. + private long oldTime; + private long startTime; + + private float lastTPF, lastFPS; + + private final static long LWJGL_TIMER_RES = 1; + private final static float INV_LWJGL_TIMER_RES = ( 1f / LWJGL_TIMER_RES ); + public final static long LWJGL_TIME_TO_NANOS = (1000000000 / LWJGL_TIMER_RES); + + /** + * Constructor builds a Timer object. All values will be + * initialized to it's default values. + */ + public LwjglTimer() { + reset(); + logger.log(Level.FINE, "Timer resolution: {0} ticks per second", LWJGL_TIMER_RES); + } + + public void reset() { + startTime = (long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS); + oldTime = getTime(); + } + + @Override + public float getTimeInSeconds() { + return getTime() * INV_LWJGL_TIMER_RES; + } + + /** + * @see Timer#getTime() + */ + public long getTime() { + return ((long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS) - startTime); + } + + /** + * @see Timer#getResolution() + */ + public long getResolution() { + return LWJGL_TIMER_RES; + } + + /** + * getFrameRate returns the current frame rate since the last + * call to update. + * + * @return the current frame rate. + */ + public float getFrameRate() { + return lastFPS; + } + + public float getTimePerFrame() { + return lastTPF; + } + + /** + * update recalulates the frame rate based on the previous + * call to update. It is assumed that update is called each frame. + */ + public void update() { + long curTime = getTime(); + lastTPF = (curTime - oldTime) * (1.0f / LWJGL_TIMER_RES); + lastFPS = 1.0f / lastTPF; + oldTime = curTime; + } + + /** + * toString returns the string representation of this timer + * in the format:
+ *
+ * jme.utility.Timer@1db699b
+ * Time: {LONG}
+ * FPS: {LONG}
+ * + * @return the string representation of this object. + */ + @Override + public String toString() { + String string = super.toString(); + string += "\nTime: " + oldTime; + string += "\nFPS: " + getFrameRate(); + return string; + } +} \ No newline at end of file diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java new file mode 100644 index 000000000..c089bf28d --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java @@ -0,0 +1,489 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.system.lwjgl; + +import com.jme3.input.JoyInput; +import com.jme3.input.KeyInput; +import com.jme3.input.MouseInput; +import com.jme3.input.TouchInput; +import com.jme3.input.lwjgl.GlfwJoystickInput; +import com.jme3.input.lwjgl.LwjglKeyInput; +import com.jme3.input.lwjgl.LwjglMouseInput; +import com.jme3.system.AppSettings; +import com.jme3.system.JmeContext; +import com.jme3.system.JmeSystem; +import org.lwjgl.PointerBuffer; +import org.lwjgl.Sys; +import org.lwjgl.glfw.*; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL11.GL_FALSE; +import static org.lwjgl.opengl.GL11.GL_TRUE; +import static org.lwjgl.system.MemoryUtil.NULL; + +/** + * A wrapper class over the GLFW framework in LWJGL 3. + * + * @author Daniel Johansson (dannyjo) + * @since 3.1 + */ +public abstract class LwjglWindow extends LwjglContext implements Runnable { + + private static final Logger LOGGER = Logger.getLogger(LwjglWindow.class.getName()); + + protected AtomicBoolean needClose = new AtomicBoolean(false); + protected final AtomicBoolean needRestart = new AtomicBoolean(false); + protected boolean wasActive = false; + protected boolean autoFlush = true; + protected boolean allowSwapBuffers = false; + private long window = -1; + private final JmeContext.Type type; + + private GLFWErrorCallback errorCallback; + private GLFWWindowSizeCallback windowSizeCallback; + private GLFWWindowFocusCallback windowFocusCallback; + + public LwjglWindow(final JmeContext.Type type) { + if (!JmeContext.Type.Display.equals(type) && !JmeContext.Type.OffscreenSurface.equals(type) && !JmeContext.Type.Canvas.equals(type)) { + throw new IllegalArgumentException("Unsupported type '" + type.name() + "' provided"); + } + + this.type = type; + } + + /** + * @return Type.Display or Type.Canvas + */ + public JmeContext.Type getType() { + return type; + } + + /** + * Set the title if its a windowed display + * + * @param title + */ + public void setTitle(final String title) { + if (created.get() && window != -1) { + glfwSetWindowTitle(window, title); + } + } + + /** + * Restart if its a windowed or full-screen display. + */ + public void restart() { + if (created.get()) { + needRestart.set(true); + } else { + LOGGER.warning("Display is not created, cannot restart window."); + } + } + + /** + * Apply the settings, changing resolution, etc. + * + * @param settings + */ + protected void createContext(final AppSettings settings) { + glfwSetErrorCallback(errorCallback = new GLFWErrorCallback() { + @Override + public void invoke(int error, long description) { + final String message = Callbacks.errorCallbackDescriptionString(description); + listener.handleError(message, new Exception(message)); + } + }); + + if (glfwInit() != GL_TRUE) { + throw new IllegalStateException("Unable to initialize GLFW"); + } + + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_VISIBLE, GL_FALSE); + + // TODO: Add support for monitor selection + long monitor = NULL; + + if (settings.isFullscreen()) { + monitor = glfwGetPrimaryMonitor(); + } + + final ByteBuffer videoMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + + if (settings.getWidth() <= 0 || settings.getHeight() <= 0) { + settings.setResolution(GLFWvidmode.width(videoMode), GLFWvidmode.height(videoMode)); + } + + window = glfwCreateWindow(settings.getWidth(), settings.getHeight(), settings.getTitle(), monitor, NULL); + + if (window == NULL) { + throw new RuntimeException("Failed to create the GLFW window"); + } + + glfwWindowHint(GLFW_RESIZABLE, settings.isResizable() ? GL_TRUE : GL_FALSE); + glfwWindowHint(GLFW_DEPTH_BITS, settings.getDepthBits()); + glfwWindowHint(GLFW_STENCIL_BITS, settings.getStencilBits()); + glfwWindowHint(GLFW_SAMPLES, settings.getSamples()); + glfwWindowHint(GLFW_STEREO, settings.useStereo3D() ? GL_TRUE : GL_FALSE); + + int frameRateCap = settings.getFrameRate(); + + if (!autoFlush) { + frameRateCap = 20; + } + + if (frameRateCap > 0) { + glfwWindowHint(GLFW_REFRESH_RATE, frameRateCap); + } + + // Not sure how else to support bits per pixel + if (settings.getBitsPerPixel() == 24) { + glfwWindowHint(GLFW_RED_BITS, 8); + glfwWindowHint(GLFW_GREEN_BITS, 8); + glfwWindowHint(GLFW_BLUE_BITS, 8); + } else if (settings.getBitsPerPixel() == 16) { + glfwWindowHint(GLFW_RED_BITS, 5); + glfwWindowHint(GLFW_GREEN_BITS, 6); + glfwWindowHint(GLFW_BLUE_BITS, 5); + } + + glfwWindowHint(GLFW_ALPHA_BITS, settings.getAlphaBits()); + + glfwSetWindowFocusCallback(window, windowFocusCallback = new GLFWWindowFocusCallback() { + @Override + public void invoke(final long window, final int focused) { + final boolean focus = (focused == 1); + + if (wasActive != focus) { + if (!wasActive) { + listener.gainFocus(); + timer.reset(); + } else { + listener.loseFocus(); + } + + wasActive = !wasActive; + } + + } + }); + + // Center the window + if (!settings.isFullscreen() && Type.Display.equals(type)) { + glfwSetWindowPos(window, (GLFWvidmode.width(videoMode) - settings.getWidth()) / 2, (GLFWvidmode.height(videoMode) - settings.getHeight()) / 2); + } + + // Make the OpenGL context current + glfwMakeContextCurrent(window); + + // Enable vsync + if (settings.isVSync()) { + glfwSwapInterval(1); + } else { + glfwSwapInterval(0); + } + + + // Make the window visible + if (Type.Display.equals(type)) { + glfwShowWindow(window); + } + + // Add a resize callback which delegates to the listener + glfwSetWindowSizeCallback(window, windowSizeCallback = new GLFWWindowSizeCallback() { + @Override + public void invoke(final long window, final int width, final int height) { + settings.setResolution(width, height); + listener.reshape(width, height); + } + }); + + allowSwapBuffers = settings.isSwapBuffers(); + + // TODO: When GLFW 3.2 is released and included in LWJGL 3.x then we should hopefully be able to set the window icon. + } + + /** + * Destroy the context. + */ + protected void destroyContext() { + try { + if (renderer != null) { + renderer.cleanup(); + } + + errorCallback.release(); + windowSizeCallback.release(); + windowFocusCallback.release(); + + if (window != 0) { + glfwDestroyWindow(window); + } + //glfwTerminate(); + } catch (Exception ex) { + listener.handleError("Failed to destroy context", ex); + } + } + + public void create(boolean waitFor) { + if (created.get()) { + LOGGER.warning("create() called when display is already created!"); + return; + } + + new Thread(this, THREAD_NAME).start(); + if (waitFor) + waitFor(true); + } + + /** + * Does LWJGL display initialization in the OpenGL thread + */ + protected boolean initInThread() { + try { + if (!JmeSystem.isLowPermissions()) { + // Enable uncaught exception handler only for current thread + Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + public void uncaughtException(Thread thread, Throwable thrown) { + listener.handleError("Uncaught exception thrown in " + thread.toString(), thrown); + if (needClose.get()) { + // listener.handleError() has requested the + // context to close. Satisfy request. + deinitInThread(); + } + } + }); + } + + timer = new LwjglTimer(); + + // For canvas, this will create a pbuffer, + // allowing us to query information. + // When the canvas context becomes available, it will + // be replaced seamlessly. + createContext(settings); + printContextInitInfo(); + + created.set(true); + super.internalCreate(); + } catch (Exception ex) { + try { + if (window != -1) { + //glfwSetWindowShouldClose(window, GL_TRUE); + glfwDestroyWindow(window); + } + } catch (Exception ex2) { + LOGGER.log(Level.WARNING, null, ex2); + } + + listener.handleError("Failed to create display", ex); + return false; // if we failed to create display, do not continue + } + + listener.initialize(); + return true; + } + + /** + * execute one iteration of the render loop in the OpenGL thread + */ + protected void runLoop() { + // If a restart is required, lets recreate the context. + if (needRestart.getAndSet(false)) { + try { + createContext(settings); + } catch (Exception ex) { + LOGGER.log(Level.SEVERE, "Failed to set display settings!", ex); + } + + LOGGER.fine("Display restarted."); + } + + if (!created.get()) { + throw new IllegalStateException(); + } + + listener.update(); + + // All this does is call swap buffers + // If the canvas is not active, there's no need to waste time + // doing that .. + if (renderable.get()) { + // calls swap buffers, etc. + try { + if (allowSwapBuffers && autoFlush) { + glfwSwapBuffers(window); + } + } catch (Throwable ex) { + listener.handleError("Error while swapping buffers", ex); + } + } + + if (glfwGetWindowAttrib(window, GLFW_FOCUSED) == GL_TRUE) { + glfwPollEvents(); + } + + // Subclasses just call GLObjectManager clean up objects here + // it is safe .. for now. + if (renderer != null) { + renderer.postFrame(); + } + } + + /** + * De-initialize in the OpenGL thread. + */ + protected void deinitInThread() { + destroyContext(); + + listener.destroy(); + LOGGER.fine("Display destroyed."); + super.internalDestroy(); + } + + public void run() { + if (listener == null) { + throw new IllegalStateException("SystemListener is not set on context!" + + "Must set with JmeContext.setSystemListner()."); + } + + registerNatives(); + loadNatives(); + LOGGER.log(Level.FINE, "Using LWJGL {0}", Sys.getVersion()); + + if (!initInThread()) { + LOGGER.log(Level.SEVERE, "Display initialization failed. Cannot continue."); + return; + } + + while (true) { + if (glfwWindowShouldClose(window) == GL_TRUE) { + listener.requestClose(false); + } + + runLoop(); + + if (needClose.get()) { + break; + } + } + + deinitInThread(); + } + + public JoyInput getJoyInput() { + if (joyInput == null) { + joyInput = new GlfwJoystickInput(); + } + return joyInput; + } + + public MouseInput getMouseInput() { + if (mouseInput == null) { + mouseInput = new LwjglMouseInput(this); + } + return mouseInput; + } + + public KeyInput getKeyInput() { + if (keyInput == null) { + keyInput = new LwjglKeyInput(this); + } + + return keyInput; + } + + public TouchInput getTouchInput() { + return null; + } + + public void setAutoFlushFrames(boolean enabled) { + this.autoFlush = enabled; + } + + public void destroy(boolean waitFor) { + needClose.set(true); + + if (waitFor) { + waitFor(false); + } + } + + public long getWindowHandle() { + return window; + } + + private ByteBuffer[] imagesToByteBuffers(Object[] images) { + ByteBuffer[] out = new ByteBuffer[images.length]; + for (int i = 0; i < images.length; i++) { + BufferedImage image = (BufferedImage) images[i]; + out[i] = imageToByteBuffer(image); + } + return out; + } + + private ByteBuffer imageToByteBuffer(BufferedImage image) { + if (image.getType() != BufferedImage.TYPE_INT_ARGB_PRE) { + BufferedImage convertedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE); + Graphics2D g = convertedImage.createGraphics(); + double width = image.getWidth() * (double) 1; + double height = image.getHeight() * (double) 1; + g.drawImage(image, (int) ((convertedImage.getWidth() - width) / 2), + (int) ((convertedImage.getHeight() - height) / 2), + (int) (width), (int) (height), null); + g.dispose(); + image = convertedImage; + } + + byte[] imageBuffer = new byte[image.getWidth() * image.getHeight() * 4]; + int counter = 0; + for (int i = 0; i < image.getHeight(); i++) { + for (int j = 0; j < image.getWidth(); j++) { + int colorSpace = image.getRGB(j, i); + imageBuffer[counter + 0] = (byte) ((colorSpace << 8) >> 24); + imageBuffer[counter + 1] = (byte) ((colorSpace << 16) >> 24); + imageBuffer[counter + 2] = (byte) ((colorSpace << 24) >> 24); + imageBuffer[counter + 3] = (byte) (colorSpace >> 24); + counter += 4; + } + } + return ByteBuffer.wrap(imageBuffer); + } +} diff --git a/settings.gradle b/settings.gradle index 89b8fc075..8b9000340 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,6 +15,7 @@ include 'jme3-desktop' include 'jme3-blender' include 'jme3-jogl' include 'jme3-lwjgl' +include 'jme3-lwjgl3' // Other external dependencies include 'jme3-jbullet' From 8f77dca9314e4b2090acc4d30e80df50547cff1a Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Wed, 26 Aug 2015 22:24:24 +0100 Subject: [PATCH 10/94] Added jme3-lwjgl3 module which ultimately adds support for LWJGL 3.x and GLFW. --- common.gradle | 3 + .../resources/joystick-mapping.properties | 6 + .../com/jme3/system/NativeLibraryLoader.java | 22 +- .../system/lwjgl/LwjglAbstractDisplay.java | 2 +- .../com/jme3/system/lwjgl/LwjglContext.java | 28 +- jme3-lwjgl3/build.gradle | 12 + .../java/com/jme3/audio/lwjgl/LwjglAL.java | 140 +++++ .../java/com/jme3/audio/lwjgl/LwjglALC.java | 58 +++ .../java/com/jme3/audio/lwjgl/LwjglEFX.java | 66 +++ .../jme3/input/lwjgl/GlfwJoystickInput.java | 213 ++++++++ .../com/jme3/input/lwjgl/LwjglKeyInput.java | 116 +++++ .../com/jme3/input/lwjgl/LwjglMouseInput.java | 189 +++++++ .../java/com/jme3/renderer/lwjgl/LwjglGL.java | 458 ++++++++++++++++ .../com/jme3/renderer/lwjgl/LwjglGLExt.java | 83 +++ .../jme3/renderer/lwjgl/LwjglGLFboEXT.java | 99 ++++ .../jme3/renderer/lwjgl/LwjglGLFboGL3.java | 97 ++++ .../com/jme3/system/lwjgl/LwjglCanvas.java | 369 +++++++++++++ .../com/jme3/system/lwjgl/LwjglContext.java | 289 +++++++++++ .../com/jme3/system/lwjgl/LwjglDisplay.java | 12 + .../lwjgl/LwjglGLDebugOutputHandler.java | 78 +++ .../system/lwjgl/LwjglOffscreenBuffer.java | 14 + .../system/lwjgl/LwjglSmoothingTimer.java | 203 ++++++++ .../com/jme3/system/lwjgl/LwjglTimer.java | 139 +++++ .../com/jme3/system/lwjgl/LwjglWindow.java | 489 ++++++++++++++++++ settings.gradle | 1 + 25 files changed, 3158 insertions(+), 28 deletions(-) create mode 100644 jme3-lwjgl3/build.gradle create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglAL.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java create mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java diff --git a/common.gradle b/common.gradle index 69b695ecd..1102f49ad 100644 --- a/common.gradle +++ b/common.gradle @@ -16,6 +16,9 @@ repositories { maven { url "http://nifty-gui.sourceforge.net/nifty-maven-repo" } + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + } } configurations { diff --git a/jme3-core/src/main/resources/joystick-mapping.properties b/jme3-core/src/main/resources/joystick-mapping.properties index d7ce2f50a..966832036 100644 --- a/jme3-core/src/main/resources/joystick-mapping.properties +++ b/jme3-core/src/main/resources/joystick-mapping.properties @@ -63,3 +63,9 @@ Xbox\ 360\ Wireless\ Receiver.AXIS_RX=z Xbox\ 360\ Wireless\ Receiver.AXIS_RY=rz Xbox\ 360\ Wireless\ Receiver.z=AXIS_RX Xbox\ 360\ Wireless\ Receiver.rz=AXIS_RY + +# Microsoft PC-joystick driver +Microsoft\ PC-joystick\ driver.12=POV +Y +Microsoft\ PC-joystick\ driver.13=POV +X +Microsoft\ PC-joystick\ driver.14=POV -Y +Microsoft\ PC-joystick\ driver.15=POV -X diff --git a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java index 2917350f6..6e83e0e6b 100644 --- a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java +++ b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java @@ -48,9 +48,8 @@ import java.util.logging.Logger; /** * Utility class to register, extract, and load native libraries. *
- * Register your own libraries via the - * {@link #registerNativeLibrary(java.lang.String, com.jme3.system.Platform, java.lang.String, boolean) } - * method, for each platform. + * Register your own libraries via the {@link #registerNativeLibrary(String, Platform, String, String)} method, for + * each platform. * You can then extract this library (depending on platform), by * using {@link #loadNativeLibrary(java.lang.String, boolean) }. *
@@ -125,23 +124,6 @@ public final class NativeLibraryLoader { } static { - // LWJGL - registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/lwjgl.dll"); - registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/lwjgl64.dll"); - registerNativeLibrary("lwjgl", Platform.Linux32, "native/linux/liblwjgl.so"); - registerNativeLibrary("lwjgl", Platform.Linux64, "native/linux/liblwjgl64.so"); - registerNativeLibrary("lwjgl", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); - registerNativeLibrary("lwjgl", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); - - // OpenAL - // For OSX: Need to add lib prefix when extracting - registerNativeLibrary("openal", Platform.Windows32, "native/windows/OpenAL32.dll"); - registerNativeLibrary("openal", Platform.Windows64, "native/windows/OpenAL64.dll"); - registerNativeLibrary("openal", Platform.Linux32, "native/linux/libopenal.so"); - registerNativeLibrary("openal", Platform.Linux64, "native/linux/libopenal64.so"); - registerNativeLibrary("openal", Platform.MacOSX32, "native/macosx/openal.dylib", "libopenal.dylib"); - registerNativeLibrary("openal", Platform.MacOSX64, "native/macosx/openal.dylib", "libopenal.dylib"); - // BulletJme registerNativeLibrary("bulletjme", Platform.Windows32, "native/windows/x86/bulletjme.dll"); registerNativeLibrary("bulletjme", Platform.Windows64, "native/windows/x86_64/bulletjme.dll"); diff --git a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java index 7cdca0a57..415084788 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java +++ b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java @@ -40,7 +40,6 @@ import com.jme3.input.lwjgl.JInputJoyInput; import com.jme3.input.lwjgl.LwjglKeyInput; import com.jme3.input.lwjgl.LwjglMouseInput; import com.jme3.system.AppSettings; -import com.jme3.system.JmeContext.Type; import com.jme3.system.JmeSystem; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; @@ -207,6 +206,7 @@ public abstract class LwjglAbstractDisplay extends LwjglContext implements Runna + "Must set with JmeContext.setSystemListner()."); } + registerNatives(); loadNatives(); logger.log(Level.FINE, "Using LWJGL {0}", Sys.getVersion()); if (!initInThread()) { diff --git a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java index c88f7b734..70885c39b 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java +++ b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -52,12 +52,8 @@ import com.jme3.renderer.opengl.GLRenderer; import com.jme3.renderer.opengl.GLTiming; import com.jme3.renderer.opengl.GLTimingState; import com.jme3.renderer.opengl.GLTracer; -import com.jme3.system.AppSettings; -import com.jme3.system.JmeContext; -import com.jme3.system.JmeSystem; -import com.jme3.system.NativeLibraryLoader; -import com.jme3.system.SystemListener; -import com.jme3.system.Timer; +import com.jme3.system.*; + import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -166,7 +162,25 @@ public abstract class LwjglContext implements JmeContext { } } } - + + protected void registerNatives() { + // LWJGL + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/lwjgl.dll"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/lwjgl64.dll"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux32, "native/linux/liblwjgl.so"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux64, "native/linux/liblwjgl64.so"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); + + // For OSX: Need to add lib prefix when extracting + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows32, "native/windows/OpenAL32.dll"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows64, "native/windows/OpenAL64.dll"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux32, "native/linux/libopenal.so"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux64, "native/linux/libopenal64.so"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX32, "native/macosx/openal.dylib", "libopenal.dylib"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX64, "native/macosx/openal.dylib", "libopenal.dylib"); + } + protected void loadNatives() { if (JmeSystem.isLowPermissions()) { return; diff --git a/jme3-lwjgl3/build.gradle b/jme3-lwjgl3/build.gradle new file mode 100644 index 000000000..c9b43e0de --- /dev/null +++ b/jme3-lwjgl3/build.gradle @@ -0,0 +1,12 @@ +if (!hasProperty('mainClass')) { + ext.mainClass = '' +} + +dependencies { + compile project(':jme3-core') + compile project(':jme3-desktop') + compile 'org.lwjgl:lwjgl:3.0.0b-SNAPSHOT' + compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0b-SNAPSHOT', classifier: 'natives-windows' + compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0b-SNAPSHOT', classifier: 'natives-linux' + compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0b-SNAPSHOT', classifier: 'natives-osx' +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglAL.java b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglAL.java new file mode 100644 index 000000000..313bdd5ee --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglAL.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */package com.jme3.audio.lwjgl; + +import com.jme3.audio.openal.AL; +import com.jme3.system.NativeLibraryLoader; +import com.jme3.system.Platform; +import org.lwjgl.openal.AL10; +import org.lwjgl.openal.AL11; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public final class LwjglAL implements AL { + + public LwjglAL() { + } + + public String alGetString(int parameter) { + return AL10.alGetString(parameter); + } + + public int alGenSources() { + return AL10.alGenSources(); + } + + public int alGetError() { + return AL10.alGetError(); + } + + public void alDeleteSources(int numSources, IntBuffer sources) { + if (sources.position() != 0) throw new AssertionError(); + if (sources.limit() != numSources) throw new AssertionError(); + AL10.alDeleteSources(sources); + } + + public void alGenBuffers(int numBuffers, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numBuffers) throw new AssertionError(); + AL10.alGenBuffers(buffers); + } + + public void alDeleteBuffers(int numBuffers, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numBuffers) throw new AssertionError(); + AL10.alDeleteBuffers(buffers); + } + + public void alSourceStop(int source) { + AL10.alSourceStop(source); + } + + public void alSourcei(int source, int param, int value) { + AL10.alSourcei(source, param, value); + } + + public void alBufferData(int buffer, int format, ByteBuffer data, int size, int frequency) { + if (data.position() != 0) throw new AssertionError(); + if (data.limit() != size) throw new AssertionError(); + AL10.alBufferData(buffer, format, data, frequency); + } + + public void alSourcePlay(int source) { + AL10.alSourcePlay(source); + } + + public void alSourcePause(int source) { + AL10.alSourcePause(source); + } + + public void alSourcef(int source, int param, float value) { + AL10.alSourcef(source, param, value); + } + + public void alSource3f(int source, int param, float value1, float value2, float value3) { + AL10.alSource3f(source, param, value1, value2, value3); + } + + public int alGetSourcei(int source, int param) { + return AL10.alGetSourcei(source, param); + } + + public void alSourceUnqueueBuffers(int source, int numBuffers, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numBuffers) throw new AssertionError(); + AL10.alSourceUnqueueBuffers(source, buffers); + } + + public void alSourceQueueBuffers(int source, int numBuffers, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numBuffers) throw new AssertionError(); + AL10.alSourceQueueBuffers(source, buffers); + } + + public void alListener(int param, FloatBuffer data) { + AL10.alListenerfv(param, data); + } + + public void alListenerf(int param, float value) { + AL10.alListenerf(param, value); + } + + public void alListener3f(int param, float value1, float value2, float value3) { + AL10.alListener3f(param, value1, value2, value3); + } + + public void alSource3i(int source, int param, int value1, int value2, int value3) { + AL11.alSource3i(source, param, value1, value2, value3); + } + +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java new file mode 100644 index 000000000..e0aed80ee --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java @@ -0,0 +1,58 @@ +package com.jme3.audio.lwjgl; + +import com.jme3.audio.openal.ALC; +import org.lwjgl.openal.ALC10; +import org.lwjgl.openal.ALContext; + +import java.nio.IntBuffer; + +import static org.lwjgl.openal.ALC10.alcGetContextsDevice; +import static org.lwjgl.openal.ALC10.alcGetCurrentContext; + +public class LwjglALC implements ALC { + + private ALContext context; + + public void createALC() { + context = ALContext.create(); + } + + public void destroyALC() { + if (context != null) { + context.destroy(); + } + } + + public boolean isCreated() { + return context != null; + } + + public String alcGetString(final int parameter) { + final long context = alcGetCurrentContext(); + final long device = alcGetContextsDevice(context); + return ALC10.alcGetString(device, parameter); + } + + public boolean alcIsExtensionPresent(final String extension) { + final long context = alcGetCurrentContext(); + final long device = alcGetContextsDevice(context); + return ALC10.alcIsExtensionPresent(device, extension); + } + + public void alcGetInteger(final int param, final IntBuffer buffer, final int size) { + if (buffer.position() != 0) throw new AssertionError(); + if (buffer.limit() != size) throw new AssertionError(); + + final long context = alcGetCurrentContext(); + final long device = alcGetContextsDevice(context); + final int value = ALC10.alcGetInteger(device, param); + //buffer.put(value); + } + + public void alcDevicePauseSOFT() { + } + + public void alcDeviceResumeSOFT() { + } + +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java new file mode 100644 index 000000000..2ddd09ed8 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java @@ -0,0 +1,66 @@ +package com.jme3.audio.lwjgl; + +import com.jme3.audio.openal.EFX; +import org.lwjgl.openal.EXTEfx; + +import java.nio.IntBuffer; + +public class LwjglEFX implements EFX { + + public void alGenAuxiliaryEffectSlots(int numSlots, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numSlots) throw new AssertionError(); + EXTEfx.alGenAuxiliaryEffectSlots(buffers); + } + + public void alGenEffects(int numEffects, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numEffects) throw new AssertionError(); + EXTEfx.alGenEffects(buffers); + } + + public void alEffecti(int effect, int param, int value) { + EXTEfx.alEffecti(effect, param, value); + } + + public void alAuxiliaryEffectSloti(int effectSlot, int param, int value) { + EXTEfx.alAuxiliaryEffectSloti(effectSlot, param, value); + } + + public void alDeleteEffects(int numEffects, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numEffects) throw new AssertionError(); + EXTEfx.alDeleteEffects(buffers); + } + + public void alDeleteAuxiliaryEffectSlots(int numEffectSlots, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numEffectSlots) throw new AssertionError(); + EXTEfx.alDeleteAuxiliaryEffectSlots(buffers); + } + + public void alGenFilters(int numFilters, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numFilters) throw new AssertionError(); + EXTEfx.alGenFilters(buffers); + } + + public void alFilteri(int filter, int param, int value) { + EXTEfx.alFilteri(filter, param, value); + } + + public void alFilterf(int filter, int param, float value) { + EXTEfx.alFilterf(filter, param, value); + } + + public void alDeleteFilters(int numFilters, IntBuffer buffers) { + if (buffers.position() != 0) throw new AssertionError(); + if (buffers.limit() != numFilters) throw new AssertionError(); + EXTEfx.alDeleteFilters(buffers); + } + + public void alEffectf(int effect, int param, float value) { + EXTEfx.alEffectf(effect, param, value); + } + +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java new file mode 100644 index 000000000..f17c08534 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.input.lwjgl; + +import com.jme3.input.*; +import com.jme3.input.event.JoyAxisEvent; +import com.jme3.input.event.JoyButtonEvent; +import org.lwjgl.opengl.GL11; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.*; + +/** + * @author Daniel Johansson (dannyjo) + * @since 3.1 + */ +public class GlfwJoystickInput implements JoyInput { + + private static final Logger LOGGER = Logger.getLogger(InputManager.class.getName()); + + private boolean initialized = false; + private RawInputListener listener; + private Map joysticks = new HashMap(); + + public void setJoyRumble(int joyId, float amount) { + if (joyId >= joysticks.size()) { + throw new IllegalArgumentException(); + } + } + + @Override + public Joystick[] loadJoysticks(final InputManager inputManager) { + // TODO: Implement + + for (int i = 0; i < GLFW_JOYSTICK_LAST; i++) { + if (glfwJoystickPresent(i) == GL11.GL_TRUE) { + final String name = glfwGetJoystickName(i); + final GlfwJoystick joystick = new GlfwJoystick(inputManager, this, i, name); + joysticks.put(i, joystick); + + final FloatBuffer floatBuffer = glfwGetJoystickAxes(i); + + int axisIndex = 0; + while (floatBuffer.hasRemaining()) { + floatBuffer.get(); + + final String logicalId = JoystickCompatibilityMappings.remapComponent(joystick.getName(), convertAxisIndex(axisIndex)); + final JoystickAxis joystickAxis = new DefaultJoystickAxis(inputManager, joystick, axisIndex, convertAxisIndex(axisIndex), logicalId, true, false, 0.0f); + joystick.addAxis(axisIndex, joystickAxis); + axisIndex++; + } + + final ByteBuffer byteBuffer = glfwGetJoystickButtons(i); + + int buttonIndex = 0; + while (byteBuffer.hasRemaining()) { + byteBuffer.get(); + final String logicalId = JoystickCompatibilityMappings.remapComponent(joystick.getName(), String.valueOf(buttonIndex)); + joystick.addButton(new DefaultJoystickButton(inputManager, joystick, buttonIndex, String.valueOf(buttonIndex), logicalId)); + buttonIndex++; + } + } + } + + return joysticks.values().toArray(new GlfwJoystick[joysticks.size()]); + } + + private String convertAxisIndex(final int index) { + if (index == 0) { + return "pov_x"; + } else if (index == 1) { + return "pov_y"; + } else if (index == 2) { + return "z"; + } else if (index == 3) { + return "rz"; + } + + return String.valueOf(index); + } + + public void initialize() { + initialized = true; + } + + public void update() { + for (final Map.Entry entry : joysticks.entrySet()) { + // Axes + final FloatBuffer axisValues = glfwGetJoystickAxes(entry.getKey()); + + for (final JoystickAxis axis : entry.getValue().getAxes()) { + final float value = axisValues.get(axis.getAxisId()); + listener.onJoyAxisEvent(new JoyAxisEvent(axis, value)); + } + + // Buttons + final ByteBuffer byteBuffer = glfwGetJoystickButtons(entry.getKey()); + + for (final JoystickButton button : entry.getValue().getButtons()) { + final boolean pressed = byteBuffer.get(button.getButtonId()) == GLFW_PRESS; + listener.onJoyButtonEvent(new JoyButtonEvent(button, pressed)); + } + } + } + + public void destroy() { + initialized = false; + } + + public boolean isInitialized() { + return initialized; + } + + public void setInputListener(RawInputListener listener) { + this.listener = listener; + } + + public long getInputTimeNanos() { + return 0; + } + + protected class GlfwJoystick extends AbstractJoystick { + + private JoystickAxis povAxisX; + private JoystickAxis povAxisY; + + public GlfwJoystick(InputManager inputManager, JoyInput joyInput, int joyId, String name) { + super(inputManager, joyInput, joyId, name); + } + + public void addAxis(final int index, final JoystickAxis axis) { + super.addAxis(axis); + + if (index == 0) { + povAxisX = axis; + } else if (index == 1) { + povAxisY = axis; + } + } + + @Override + protected void addButton(JoystickButton button) { + super.addButton(button); + } + + @Override + public JoystickAxis getXAxis() { + return povAxisX; + } + + @Override + public JoystickAxis getYAxis() { + return povAxisY; + } + + @Override + public JoystickAxis getPovXAxis() { + return povAxisX; + } + + @Override + public JoystickAxis getPovYAxis() { + return povAxisY; + } + + @Override + public int getXAxisIndex() { + return povAxisX.getAxisId(); + } + + @Override + public int getYAxisIndex() { + return povAxisY.getAxisId(); + } + } +} + + + diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java new file mode 100644 index 000000000..a39010c95 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.input.lwjgl; + +import com.jme3.input.KeyInput; +import com.jme3.input.RawInputListener; +import com.jme3.input.event.KeyInputEvent; +import com.jme3.system.lwjgl.LwjglTimer; +import com.jme3.system.lwjgl.LwjglWindow; +import org.lwjgl.glfw.GLFWKeyCallback; + +import java.util.LinkedList; +import java.util.Queue; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.*; + +public class LwjglKeyInput implements KeyInput { + + private static final Logger logger = Logger.getLogger(LwjglKeyInput.class.getName()); + + private LwjglWindow context; + private RawInputListener listener; + private boolean initialized; + private GLFWKeyCallback keyCallback; + private Queue keyInputEvents = new LinkedList(); + + public LwjglKeyInput(LwjglWindow context) { + this.context = context; + } + + public void initialize() { + if (!context.isRenderable()) { + return; + } + + glfwSetKeyCallback(context.getWindowHandle(), keyCallback = new GLFWKeyCallback() { + @Override + public void invoke(long window, int key, int scancode, int action, int mods) { + final KeyInputEvent evt = new KeyInputEvent(scancode, (char) key, GLFW_PRESS == action, GLFW_REPEAT == action); + evt.setTime(getInputTimeNanos()); + keyInputEvents.add(evt); + } + }); + + glfwSetInputMode(context.getWindowHandle(), GLFW_STICKY_KEYS, 1); + + initialized = true; + logger.fine("Keyboard created."); + } + + public int getKeyCount() { + return 0; // TODO: How do we figure this out? + } + + public void update() { + if (!context.isRenderable()) { + return; + } + + while (!keyInputEvents.isEmpty()) { + listener.onKeyEvent(keyInputEvents.poll()); + } + } + + public void destroy() { + if (!context.isRenderable()) { + return; + } + + keyCallback.release(); + logger.fine("Keyboard destroyed."); + } + + public boolean isInitialized() { + return initialized; + } + + public void setInputListener(RawInputListener listener) { + this.listener = listener; + } + + public long getInputTimeNanos() { + return (long) (glfwGetTime() * LwjglTimer.LWJGL_TIME_TO_NANOS); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java new file mode 100644 index 000000000..45262ffbc --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.input.lwjgl; + +import com.jme3.cursors.plugins.JmeCursor; +import com.jme3.input.MouseInput; +import com.jme3.input.RawInputListener; +import com.jme3.input.event.MouseButtonEvent; +import com.jme3.input.event.MouseMotionEvent; +import com.jme3.system.lwjgl.LwjglTimer; +import com.jme3.system.lwjgl.LwjglWindow; +import org.lwjgl.glfw.GLFWCursorPosCallback; +import org.lwjgl.glfw.GLFWMouseButtonCallback; +import org.lwjgl.glfw.GLFWScrollCallback; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.Queue; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.*; + +public class LwjglMouseInput implements MouseInput { + + private static final Logger logger = Logger.getLogger(LwjglMouseInput.class.getName()); + + private LwjglWindow context; + private RawInputListener listener; + private boolean cursorVisible = true; + private int mouseX; + private int mouseY; + private int mouseWheel; + private boolean initialized; + private GLFWCursorPosCallback cursorPosCallback; + private GLFWScrollCallback scrollCallback; + private GLFWMouseButtonCallback mouseButtonCallback; + private Queue mouseMotionEvents = new LinkedList(); + private Queue mouseButtonEvents = new LinkedList(); + + public LwjglMouseInput(LwjglWindow context) { + this.context = context; + } + + public void initialize() { + glfwSetCursorPosCallback(context.getWindowHandle(), cursorPosCallback = new GLFWCursorPosCallback() { + @Override + public void invoke(long window, double xpos, double ypos) { + int xDelta; + int yDelta; + int x = (int) Math.round(xpos); + int y = (int) Math.round(ypos); + + if (mouseX == 0) { + mouseX = x; + } + + if (mouseY == 0) { + mouseY = y; + } + + xDelta = x - mouseX; + yDelta = y - mouseY; + mouseX = x; + mouseY = y; + + if (xDelta != 0 || yDelta != 0) { + final MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(x, y * -1, xDelta, yDelta * -1, mouseWheel, 0); + mouseMotionEvent.setTime(getInputTimeNanos()); + mouseMotionEvents.add(mouseMotionEvent); + } + } + }); + + glfwSetScrollCallback(context.getWindowHandle(), scrollCallback = new GLFWScrollCallback() { + @Override + public void invoke(final long window, final double xOffset, final double yOffset) { + mouseWheel += yOffset; + + final MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(mouseX, mouseY, 0, 0, mouseWheel, (int) Math.round(yOffset)); + mouseMotionEvent.setTime(getInputTimeNanos()); + mouseMotionEvents.add(mouseMotionEvent); + } + }); + + glfwSetMouseButtonCallback(context.getWindowHandle(), mouseButtonCallback = new GLFWMouseButtonCallback() { + @Override + public void invoke(final long window, final int button, final int action, final int mods) { + final MouseButtonEvent mouseButtonEvent = new MouseButtonEvent(button, action == GLFW_PRESS, mouseX, mouseY); + mouseButtonEvent.setTime(getInputTimeNanos()); + mouseButtonEvents.add(mouseButtonEvent); + } + }); + + setCursorVisible(cursorVisible); + logger.fine("Mouse created."); + initialized = true; + } + + public boolean isInitialized() { + return initialized; + } + + public int getButtonCount() { + return 2; // TODO: How to determine this? + } + + public void update() { + while (!mouseMotionEvents.isEmpty()) { + listener.onMouseMotionEvent(mouseMotionEvents.poll()); + } + + while (!mouseButtonEvents.isEmpty()) { + listener.onMouseButtonEvent(mouseButtonEvents.poll()); + } + } + + public void destroy() { + if (!context.isRenderable()) { + return; + } + + cursorPosCallback.release(); + scrollCallback.release(); + mouseButtonCallback.release(); + + logger.fine("Mouse destroyed."); + } + + public void setCursorVisible(boolean visible) { + cursorVisible = visible; + + if (!context.isRenderable()) { + return; + } + + if (cursorVisible) { + glfwSetInputMode(context.getWindowHandle(), GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } else { + glfwSetInputMode(context.getWindowHandle(), GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } + } + + public void setInputListener(RawInputListener listener) { + this.listener = listener; + } + + public long getInputTimeNanos() { + return (long) (glfwGetTime() * LwjglTimer.LWJGL_TIME_TO_NANOS); + } + + public void setNativeCursor(final JmeCursor jmeCursor) { + if (jmeCursor != null) { + final ByteBuffer byteBuffer = org.lwjgl.BufferUtils.createByteBuffer(jmeCursor.getImagesData().capacity()); + byteBuffer.asIntBuffer().put(jmeCursor.getImagesData().array()); + final long cursor = glfwCreateCursor(byteBuffer, jmeCursor.getXHotSpot(), jmeCursor.getYHotSpot()); + glfwSetCursor(context.getWindowHandle(), cursor); + } + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java new file mode 100644 index 000000000..80e7f3513 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java @@ -0,0 +1,458 @@ +package com.jme3.renderer.lwjgl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GL; +import com.jme3.renderer.opengl.GL2; +import com.jme3.renderer.opengl.GL3; +import com.jme3.renderer.opengl.GL4; +import com.jme3.system.NativeLibraryLoader; +import com.jme3.system.Platform; +import org.lwjgl.opengl.*; + +import java.nio.*; + +public class LwjglGL implements GL, GL2, GL3, GL4 { + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + public void resetStats() { + } + + public void glActiveTexture(int param1) { + GL13.glActiveTexture(param1); + } + + public void glAlphaFunc(int param1, float param2) { + GL11.glAlphaFunc(param1, param2); + } + + public void glAttachShader(int param1, int param2) { + GL20.glAttachShader(param1, param2); + } + + public void glBindBuffer(int param1, int param2) { + GL15.glBindBuffer(param1, param2); + } + + public void glBindTexture(int param1, int param2) { + GL11.glBindTexture(param1, param2); + } + + public void glBlendFunc(int param1, int param2) { + GL11.glBlendFunc(param1, param2); + } + + public void glBufferData(int param1, long param2, int param3) { + GL15.glBufferData(param1, param2, param3); + } + + public void glBufferData(int param1, FloatBuffer param2, int param3) { + checkLimit(param2); + GL15.glBufferData(param1, param2, param3); + } + + public void glBufferData(int param1, ShortBuffer param2, int param3) { + checkLimit(param2); + GL15.glBufferData(param1, param2, param3); + } + + public void glBufferData(int param1, ByteBuffer param2, int param3) { + checkLimit(param2); + GL15.glBufferData(param1, param2, param3); + } + + public void glBufferSubData(int param1, long param2, FloatBuffer param3) { + checkLimit(param3); + GL15.glBufferSubData(param1, param2, param3); + } + + public void glBufferSubData(int param1, long param2, ShortBuffer param3) { + checkLimit(param3); + GL15.glBufferSubData(param1, param2, param3); + } + + public void glBufferSubData(int param1, long param2, ByteBuffer param3) { + checkLimit(param3); + GL15.glBufferSubData(param1, param2, param3); + } + + public void glClear(int param1) { + GL11.glClear(param1); + } + + public void glClearColor(float param1, float param2, float param3, float param4) { + GL11.glClearColor(param1, param2, param3, param4); + } + + public void glColorMask(boolean param1, boolean param2, boolean param3, boolean param4) { + GL11.glColorMask(param1, param2, param3, param4); + } + + public void glCompileShader(int param1) { + GL20.glCompileShader(param1); + } + + public void glCompressedTexImage2D(int param1, int param2, int param3, int param4, int param5, int param6, ByteBuffer param7) { + checkLimit(param7); + GL13.glCompressedTexImage2D(param1, param2, param3, param4, param5, param6, param7); + } + + public void glCompressedTexImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, ByteBuffer param8) { + checkLimit(param8); + GL13.glCompressedTexImage3D(param1, param2, param3, param4, param5, param6, param7, param8); + } + + public void glCompressedTexSubImage2D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, ByteBuffer param8) { + checkLimit(param8); + GL13.glCompressedTexSubImage2D(param1, param2, param3, param4, param5, param6, param7, param8); + } + + public void glCompressedTexSubImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, int param9, ByteBuffer param10) { + checkLimit(param10); + GL13.glCompressedTexSubImage3D(param1, param2, param3, param4, param5, param6, param7, param8, param9, param10); + } + + public int glCreateProgram() { + return GL20.glCreateProgram(); + } + + public int glCreateShader(int param1) { + return GL20.glCreateShader(param1); + } + + public void glCullFace(int param1) { + GL11.glCullFace(param1); + } + + public void glDeleteBuffers(IntBuffer param1) { + checkLimit(param1); + GL15.glDeleteBuffers(param1); + } + + public void glDeleteProgram(int param1) { + GL20.glDeleteProgram(param1); + } + + public void glDeleteShader(int param1) { + GL20.glDeleteShader(param1); + } + + public void glDeleteTextures(IntBuffer param1) { + checkLimit(param1); + GL11.glDeleteTextures(param1); + } + + public void glDepthFunc(int param1) { + GL11.glDepthFunc(param1); + } + + public void glDepthMask(boolean param1) { + GL11.glDepthMask(param1); + } + + public void glDepthRange(double param1, double param2) { + GL11.glDepthRange(param1, param2); + } + + public void glDetachShader(int param1, int param2) { + GL20.glDetachShader(param1, param2); + } + + public void glDisable(int param1) { + GL11.glDisable(param1); + } + + public void glDisableVertexAttribArray(int param1) { + GL20.glDisableVertexAttribArray(param1); + } + + public void glDrawArrays(int param1, int param2, int param3) { + GL11.glDrawArrays(param1, param2, param3); + } + + public void glDrawBuffer(int param1) { + GL11.glDrawBuffer(param1); + } + + public void glDrawRangeElements(int param1, int param2, int param3, int param4, int param5, long param6) { + GL12.glDrawRangeElements(param1, param2, param3, param4, param5, param6); + } + + public void glEnable(int param1) { + GL11.glEnable(param1); + } + + public void glEnableVertexAttribArray(int param1) { + GL20.glEnableVertexAttribArray(param1); + } + + public void glGenBuffers(IntBuffer param1) { + checkLimit(param1); + GL15.glGenBuffers(param1); + } + + public void glGenTextures(IntBuffer param1) { + checkLimit(param1); + GL11.glGenTextures(param1); + } + + public void glGetBoolean(int param1, ByteBuffer param2) { + checkLimit(param2); + GL11.glGetBooleanv(param1, param2); + } + + public void glGetBufferSubData(int target, long offset, ByteBuffer data) { + checkLimit(data); + GL15.glGetBufferSubData(target, offset, data); + } + + public int glGetError() { + return GL11.glGetError(); + } + + public void glGetInteger(int param1, IntBuffer param2) { + checkLimit(param2); + GL11.glGetIntegerv(param1, param2); + } + + public void glGetProgram(int param1, int param2, IntBuffer param3) { + checkLimit(param3); + GL20.glGetProgramiv(param1, param2, param3); + } + + public void glGetShader(int param1, int param2, IntBuffer param3) { + checkLimit(param3); + GL20.glGetShaderiv(param1, param2, param3); + } + + public String glGetString(int param1) { + return GL11.glGetString(param1); + } + + public String glGetString(int param1, int param2) { + return GL30.glGetStringi(param1, param2); + } + + public boolean glIsEnabled(int param1) { + return GL11.glIsEnabled(param1); + } + + public void glLineWidth(float param1) { + GL11.glLineWidth(param1); + } + + public void glLinkProgram(int param1) { + GL20.glLinkProgram(param1); + } + + public void glPixelStorei(int param1, int param2) { + GL11.glPixelStorei(param1, param2); + } + + public void glPointSize(float param1) { + GL11.glPointSize(param1); + } + + public void glPolygonMode(int param1, int param2) { + GL11.glPolygonMode(param1, param2); + } + + public void glPolygonOffset(float param1, float param2) { + GL11.glPolygonOffset(param1, param2); + } + + public void glReadBuffer(int param1) { + GL11.glReadBuffer(param1); + } + + public void glReadPixels(int param1, int param2, int param3, int param4, int param5, int param6, ByteBuffer param7) { + checkLimit(param7); + GL11.glReadPixels(param1, param2, param3, param4, param5, param6, param7); + } + + public void glReadPixels(int param1, int param2, int param3, int param4, int param5, int param6, long param7) { + GL11.glReadPixels(param1, param2, param3, param4, param5, param6, param7); + } + + public void glScissor(int param1, int param2, int param3, int param4) { + GL11.glScissor(param1, param2, param3, param4); + } + + public void glStencilFuncSeparate(int param1, int param2, int param3, int param4) { + GL20.glStencilFuncSeparate(param1, param2, param3, param4); + } + + public void glStencilOpSeparate(int param1, int param2, int param3, int param4) { + GL20.glStencilOpSeparate(param1, param2, param3, param4); + } + + public void glTexImage2D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, ByteBuffer param9) { + checkLimit(param9); + GL11.glTexImage2D(param1, param2, param3, param4, param5, param6, param7, param8, param9); + } + + public void glTexImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, int param9, ByteBuffer param10) { + checkLimit(param10); + GL12.glTexImage3D(param1, param2, param3, param4, param5, param6, param7, param8, param9, param10); + } + + public void glTexParameterf(int param1, int param2, float param3) { + GL11.glTexParameterf(param1, param2, param3); + } + + public void glTexParameteri(int param1, int param2, int param3) { + GL11.glTexParameteri(param1, param2, param3); + } + + public void glTexSubImage2D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, ByteBuffer param9) { + checkLimit(param9); + GL11.glTexSubImage2D(param1, param2, param3, param4, param5, param6, param7, param8, param9); + } + + public void glTexSubImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, int param9, int param10, ByteBuffer param11) { + checkLimit(param11); + GL12.glTexSubImage3D(param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11); + } + + public void glUniform1(int param1, FloatBuffer param2) { + checkLimit(param2); + GL20.glUniform1fv(param1, param2); + } + + public void glUniform1(int param1, IntBuffer param2) { + checkLimit(param2); + GL20.glUniform1iv(param1, param2); + } + + public void glUniform1f(int param1, float param2) { + GL20.glUniform1f(param1, param2); + } + + public void glUniform1i(int param1, int param2) { + GL20.glUniform1i(param1, param2); + } + + public void glUniform2(int param1, IntBuffer param2) { + checkLimit(param2); + GL20.glUniform2iv(param1, param2); + } + + public void glUniform2(int param1, FloatBuffer param2) { + checkLimit(param2); + GL20.glUniform2fv(param1, param2); + } + + public void glUniform2f(int param1, float param2, float param3) { + GL20.glUniform2f(param1, param2, param3); + } + + public void glUniform3(int param1, IntBuffer param2) { + checkLimit(param2); + GL20.glUniform3iv(param1, param2); + } + + public void glUniform3(int param1, FloatBuffer param2) { + checkLimit(param2); + GL20.glUniform3fv(param1, param2); + } + + public void glUniform3f(int param1, float param2, float param3, float param4) { + GL20.glUniform3f(param1, param2, param3, param4); + } + + public void glUniform4(int param1, FloatBuffer param2) { + checkLimit(param2); + GL20.glUniform4fv(param1, param2); + } + + public void glUniform4(int param1, IntBuffer param2) { + checkLimit(param2); + GL20.glUniform4iv(param1, param2); + } + + public void glUniform4f(int param1, float param2, float param3, float param4, float param5) { + GL20.glUniform4f(param1, param2, param3, param4, param5); + } + + public void glUniformMatrix3(int param1, boolean param2, FloatBuffer param3) { + checkLimit(param3); + GL20.glUniformMatrix3fv(param1, param2, param3); + } + + public void glUniformMatrix4(int param1, boolean param2, FloatBuffer param3) { + checkLimit(param3); + GL20.glUniformMatrix4fv(param1, param2, param3); + } + + public void glUseProgram(int param1) { + GL20.glUseProgram(param1); + } + + public void glVertexAttribPointer(int param1, int param2, int param3, boolean param4, int param5, long param6) { + GL20.glVertexAttribPointer(param1, param2, param3, param4, param5, param6); + } + + public void glViewport(int param1, int param2, int param3, int param4) { + GL11.glViewport(param1, param2, param3, param4); + } + + public int glGetAttribLocation(int param1, String param2) { + // NOTE: LWJGL requires null-terminated strings + return GL20.glGetAttribLocation(param1, param2 + "\0"); + } + + public int glGetUniformLocation(int param1, String param2) { + // NOTE: LWJGL requires null-terminated strings + return GL20.glGetUniformLocation(param1, param2 + "\0"); + } + + public void glShaderSource(int param1, String[] param2, IntBuffer param3) { + checkLimit(param3); + GL20.glShaderSource(param1, param2); + } + + public String glGetProgramInfoLog(int program, int maxSize) { + return GL20.glGetProgramInfoLog(program, maxSize); + } + + public String glGetShaderInfoLog(int shader, int maxSize) { + return GL20.glGetShaderInfoLog(shader, maxSize); + } + + @Override + public void glBindFragDataLocation(int param1, int param2, String param3) { + GL30.glBindFragDataLocation(param1, param2, param3); + } + + @Override + public void glBindVertexArray(int param1) { + GL30.glBindVertexArray(param1); + } + + @Override + public void glGenVertexArrays(IntBuffer param1) { + checkLimit(param1); + GL30.glGenVertexArrays(param1); + } + + @Override + public void glPatchParameter(int count) { + GL40.glPatchParameteri(GL40.GL_PATCH_VERTICES,count); + } + + @Override + public void glDeleteVertexArrays(IntBuffer arrays) { + checkLimit(arrays); + ARBVertexArrayObject.glDeleteVertexArrays(arrays); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java new file mode 100644 index 000000000..00b3f688c --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -0,0 +1,83 @@ +package com.jme3.renderer.lwjgl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GLExt; +import org.lwjgl.opengl.*; + +import java.nio.Buffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public class LwjglGLExt implements GLExt { + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + @Override + public void glBufferData(int target, IntBuffer data, int usage) { + checkLimit(data); + GL15.glBufferData(target, data, usage); + } + + @Override + public void glBufferSubData(int target, long offset, IntBuffer data) { + checkLimit(data); + GL15.glBufferSubData(target, offset, data); + } + + @Override + public void glDrawArraysInstancedARB(int mode, int first, int count, int primcount) { + ARBDrawInstanced.glDrawArraysInstancedARB(mode, first, count, primcount); + } + + @Override + public void glDrawBuffers(IntBuffer bufs) { + checkLimit(bufs); + GL20.glDrawBuffers(bufs); + } + + @Override + public void glDrawElementsInstancedARB(int mode, int indices_count, int type, long indices_buffer_offset, int primcount) { + ARBDrawInstanced.glDrawElementsInstancedARB(mode, indices_count, type, indices_buffer_offset, primcount); + } + + @Override + public void glGetMultisample(int pname, int index, FloatBuffer val) { + checkLimit(val); + ARBTextureMultisample.glGetMultisamplefv(pname, index, val); + } + + @Override + public void glTexImage2DMultisample(int target, int samples, int internalformat, int width, int height, boolean fixedsamplelocations) { + ARBTextureMultisample.glTexImage2DMultisample(target, samples, internalformat, width, height, fixedsamplelocations); + } + + @Override + public void glVertexAttribDivisorARB(int index, int divisor) { + ARBInstancedArrays.glVertexAttribDivisorARB(index, divisor); + } + + @Override + public Object glFenceSync(int condition, int flags) { + return ARBSync.glFenceSync(condition, flags); + } + + @Override + public int glClientWaitSync(final Object sync, final int flags, final long timeout) { + return ARBSync.glClientWaitSync((Long) sync, flags, timeout); + } + + @Override + public void glDeleteSync(final Object sync) { + ARBSync.glDeleteSync((Long) sync); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java new file mode 100644 index 000000000..969d7ae1d --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java @@ -0,0 +1,99 @@ +package com.jme3.renderer.lwjgl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GLFbo; +import org.lwjgl.opengl.EXTFramebufferBlit; +import org.lwjgl.opengl.EXTFramebufferMultisample; +import org.lwjgl.opengl.EXTFramebufferObject; + +import java.nio.Buffer; +import java.nio.IntBuffer; + +/** + * Implements GLFbo via GL_EXT_framebuffer_object. + * + * @author Kirill Vainer + */ +public class LwjglGLFboEXT implements GLFbo { + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + @Override + public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) { + EXTFramebufferBlit.glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } + + @Override + public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height) { + EXTFramebufferMultisample.glRenderbufferStorageMultisampleEXT(target, samples, internalformat, width, height); + } + + @Override + public void glBindFramebufferEXT(int param1, int param2) { + EXTFramebufferObject.glBindFramebufferEXT(param1, param2); + } + + @Override + public void glBindRenderbufferEXT(int param1, int param2) { + EXTFramebufferObject.glBindRenderbufferEXT(param1, param2); + } + + @Override + public int glCheckFramebufferStatusEXT(int param1) { + return EXTFramebufferObject.glCheckFramebufferStatusEXT(param1); + } + + @Override + public void glDeleteFramebuffersEXT(IntBuffer param1) { + checkLimit(param1); + EXTFramebufferObject.glDeleteFramebuffersEXT(param1); + } + + @Override + public void glDeleteRenderbuffersEXT(IntBuffer param1) { + checkLimit(param1); + EXTFramebufferObject.glDeleteRenderbuffersEXT(param1); + } + + @Override + public void glFramebufferRenderbufferEXT(int param1, int param2, int param3, int param4) { + EXTFramebufferObject.glFramebufferRenderbufferEXT(param1, param2, param3, param4); + } + + @Override + public void glFramebufferTexture2DEXT(int param1, int param2, int param3, int param4, int param5) { + EXTFramebufferObject.glFramebufferTexture2DEXT(param1, param2, param3, param4, param5); + } + + @Override + public void glGenFramebuffersEXT(IntBuffer param1) { + checkLimit(param1); + EXTFramebufferObject.glGenFramebuffersEXT(param1); + } + + @Override + public void glGenRenderbuffersEXT(IntBuffer param1) { + checkLimit(param1); + EXTFramebufferObject.glGenRenderbuffersEXT(param1); + } + + @Override + public void glGenerateMipmapEXT(int param1) { + EXTFramebufferObject.glGenerateMipmapEXT(param1); + } + + @Override + public void glRenderbufferStorageEXT(int param1, int param2, int param3, int param4) { + EXTFramebufferObject.glRenderbufferStorageEXT(param1, param2, param3, param4); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java new file mode 100644 index 000000000..aa15aeb09 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java @@ -0,0 +1,97 @@ +package com.jme3.renderer.lwjgl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GLFbo; +import org.lwjgl.opengl.GL30; + +import java.nio.Buffer; +import java.nio.IntBuffer; + +/** + * Implements GLFbo via OpenGL3+. + * + * @author Kirill Vainer + */ +public class LwjglGLFboGL3 implements GLFbo { + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + @Override + public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) { + GL30.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } + + @Override + public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height) { + GL30.glRenderbufferStorageMultisample(target, samples, internalformat, width, height); + } + + @Override + public void glBindFramebufferEXT(int param1, int param2) { + GL30.glBindFramebuffer(param1, param2); + } + + @Override + public void glBindRenderbufferEXT(int param1, int param2) { + GL30.glBindRenderbuffer(param1, param2); + } + + @Override + public int glCheckFramebufferStatusEXT(int param1) { + return GL30.glCheckFramebufferStatus(param1); + } + + @Override + public void glDeleteFramebuffersEXT(IntBuffer param1) { + checkLimit(param1); + GL30.glDeleteFramebuffers(param1); + } + + @Override + public void glDeleteRenderbuffersEXT(IntBuffer param1) { + checkLimit(param1); + GL30.glDeleteRenderbuffers(param1); + } + + @Override + public void glFramebufferRenderbufferEXT(int param1, int param2, int param3, int param4) { + GL30.glFramebufferRenderbuffer(param1, param2, param3, param4); + } + + @Override + public void glFramebufferTexture2DEXT(int param1, int param2, int param3, int param4, int param5) { + GL30.glFramebufferTexture2D(param1, param2, param3, param4, param5); + } + + @Override + public void glGenFramebuffersEXT(IntBuffer param1) { + checkLimit(param1); + GL30.glGenFramebuffers(param1); + } + + @Override + public void glGenRenderbuffersEXT(IntBuffer param1) { + checkLimit(param1); + GL30.glGenRenderbuffers(param1); + } + + @Override + public void glGenerateMipmapEXT(int param1) { + GL30.glGenerateMipmap(param1); + } + + @Override + public void glRenderbufferStorageEXT(int param1, int param2, int param3, int param4) { + GL30.glRenderbufferStorage(param1, param2, param3, param4); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java new file mode 100644 index 000000000..1f1ef2ab5 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.system.lwjgl; + +import com.jme3.system.AppSettings; +import com.jme3.system.JmeCanvasContext; + +import javax.swing.*; +import java.awt.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.glfwDestroyWindow; + +public class LwjglCanvas extends LwjglWindow implements JmeCanvasContext { + + protected static final int TASK_NOTHING = 0, + TASK_DESTROY_DISPLAY = 1, + TASK_CREATE_DISPLAY = 2, + TASK_COMPLETE = 3; + +// protected static final boolean USE_SHARED_CONTEXT = +// Boolean.parseBoolean(System.getProperty("jme3.canvas.sharedctx", "true")); + + protected static final boolean USE_SHARED_CONTEXT = false; + + private static final Logger logger = Logger.getLogger(LwjglCanvas.class.getName()); + private Canvas canvas; + private int width; + private int height; + + private final Object taskLock = new Object(); + private int desiredTask = TASK_NOTHING; + + private Thread renderThread; + private boolean runningFirstTime = true; + private boolean mouseWasGrabbed = false; + + private boolean mouseWasCreated = false; + private boolean keyboardWasCreated = false; + + private long window; + + private class GLCanvas extends Canvas { + @Override + public void addNotify(){ + super.addNotify(); + + if (renderThread != null && renderThread.getState() == Thread.State.TERMINATED) { + return; // already destroyed. + } + + if (renderThread == null){ + logger.log(Level.FINE, "EDT: Creating OGL thread."); + + // Also set some settings on the canvas here. + // So we don't do it outside the AWT thread. + canvas.setFocusable(true); + canvas.setIgnoreRepaint(true); + + renderThread = new Thread(LwjglCanvas.this, THREAD_NAME); + renderThread.start(); + }else if (needClose.get()){ + return; + } + + logger.log(Level.FINE, "EDT: Telling OGL to create display .."); + synchronized (taskLock){ + desiredTask = TASK_CREATE_DISPLAY; +// while (desiredTask != TASK_COMPLETE){ +// try { +// taskLock.wait(); +// } catch (InterruptedException ex) { +// return; +// } +// } +// desiredTask = TASK_NOTHING; + } +// logger.log(Level.FINE, "EDT: OGL has created the display"); + } + + @Override + public void removeNotify(){ + if (needClose.get()){ + logger.log(Level.FINE, "EDT: Application is stopped. Not restoring canvas."); + super.removeNotify(); + return; + } + + // We must tell GL context to shutdown and wait for it to + // shutdown, otherwise, issues will occur. + logger.log(Level.FINE, "EDT: Telling OGL to destroy display .."); + synchronized (taskLock){ + desiredTask = TASK_DESTROY_DISPLAY; + while (desiredTask != TASK_COMPLETE){ + try { + taskLock.wait(); + } catch (InterruptedException ex){ + super.removeNotify(); + return; + } + } + desiredTask = TASK_NOTHING; + } + + logger.log(Level.FINE, "EDT: Acknowledged receipt of canvas death"); + // GL context is dead at this point + + super.removeNotify(); + } + } + + public LwjglCanvas(){ + super(Type.Canvas); + canvas = new GLCanvas(); + } + + public void create(boolean waitFor){ + if (renderThread == null){ + logger.log(Level.FINE, "MAIN: Creating OGL thread."); + + renderThread = new Thread(LwjglCanvas.this, THREAD_NAME); + renderThread.start(); + } + // do not do anything. + // superclass's create() will be called at initInThread() + if (waitFor) { + waitFor(true); + } + } + + public Canvas getCanvas(){ + return canvas; + } + + @Override + protected void runLoop(){ + if (desiredTask != TASK_NOTHING){ + synchronized (taskLock){ + switch (desiredTask){ + case TASK_CREATE_DISPLAY: + logger.log(Level.FINE, "OGL: Creating display .."); + restoreCanvas(); + listener.gainFocus(); + desiredTask = TASK_NOTHING; + break; + case TASK_DESTROY_DISPLAY: + logger.log(Level.FINE, "OGL: Destroying display .."); + listener.loseFocus(); + pauseCanvas(); + break; + } + desiredTask = TASK_COMPLETE; + taskLock.notifyAll(); + } + } + + if (renderable.get()){ + int newWidth = Math.max(canvas.getWidth(), 1); + int newHeight = Math.max(canvas.getHeight(), 1); + + if (width != newWidth || height != newHeight){ + width = newWidth; + height = newHeight; + if (listener != null){ + listener.reshape(width, height); + } + } + } + + super.runLoop(); + } + + private void pauseCanvas(){ + if (mouseInput != null) { + mouseInput.setCursorVisible(true); + mouseWasCreated = true; + } + +/* + if (Mouse.isCreated()){ + if (Mouse.isGrabbed()){ + Mouse.setGrabbed(false); + mouseWasGrabbed = true; + } + mouseWasCreated = true; + Mouse.destroy(); + } + if (Keyboard.isCreated()){ + keyboardWasCreated = true; + Keyboard.destroy(); + } +*/ + + renderable.set(false); + destroyContext(); + } + + /** + * Called to restore the canvas. + */ + private void restoreCanvas(){ + logger.log(Level.FINE, "OGL: Waiting for canvas to become displayable.."); + while (!canvas.isDisplayable()){ + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + logger.log(Level.SEVERE, "OGL: Interrupted! ", ex); + } + } + + logger.log(Level.FINE, "OGL: Creating display context .."); + + // Set renderable to true, since canvas is now displayable. + renderable.set(true); + createContext(settings); + + logger.log(Level.FINE, "OGL: Display is active!"); + + try { + if (mouseWasCreated){ +// Mouse.create(); +// if (mouseWasGrabbed){ +// Mouse.setGrabbed(true); +// mouseWasGrabbed = false; +// } + } + if (keyboardWasCreated){ +// Keyboard.create(); +// keyboardWasCreated = false; + } + } catch (Exception ex){ + logger.log(Level.SEVERE, "Encountered exception when restoring input", ex); + } + + SwingUtilities.invokeLater(new Runnable(){ + public void run(){ + canvas.requestFocus(); + } + }); + } + +/* + */ +/** + * Makes sure the pbuffer is available and ready for use + *//* + + protected void makePbufferAvailable() throws LWJGLException{ + if (pbuffer != null && pbuffer.isBufferLost()){ + logger.log(Level.WARNING, "PBuffer was lost!"); + pbuffer.destroy(); + pbuffer = null; + } + + if (pbuffer == null) { + pbuffer = new Pbuffer(1, 1, acquirePixelFormat(true), null); + pbuffer.makeCurrent(); + logger.log(Level.FINE, "OGL: Pbuffer has been created"); + + // Any created objects are no longer valid + if (!runningFirstTime){ + renderer.resetGLObjects(); + } + } + + pbuffer.makeCurrent(); + if (!pbuffer.isCurrent()){ + throw new LWJGLException("Pbuffer cannot be made current"); + } + } + + protected void destroyPbuffer(){ + if (pbuffer != null){ + if (!pbuffer.isBufferLost()){ + pbuffer.destroy(); + } + pbuffer = null; + } + } +*/ + + /** + * This is called: + * 1) When the context thread ends + * 2) Any time the canvas becomes non-displayable + */ + protected void destroyContext(){ + // invalidate the state so renderer can resume operation + if (!USE_SHARED_CONTEXT){ + renderer.cleanup(); + } + + if (window != 0) { + glfwDestroyWindow(window); + } + + // TODO: Destroy input + + + // The canvas is no longer visible, + // but the context thread is still running. + if (!needClose.get()){ + renderer.invalidateState(); + } + } + + /** + * This is called: + * 1) When the context thread starts + * 2) Any time the canvas becomes displayable again. + */ + @Override + protected void createContext(final AppSettings settings) { + // In case canvas is not visible, we still take framerate + // from settings to prevent "100% CPU usage" + allowSwapBuffers = settings.isSwapBuffers(); + + if (renderable.get()){ + if (!runningFirstTime){ + // because the display is a different opengl context + // must reset the context state. + if (!USE_SHARED_CONTEXT){ + renderer.cleanup(); + } + } + + super.createContext(settings); + } + + // At this point, the OpenGL context is active. + if (runningFirstTime) { + // THIS is the part that creates the renderer. + // It must always be called, now that we have the pbuffer workaround. + initContextFirstTime(); + runningFirstTime = false; + } + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java new file mode 100644 index 000000000..3f6491e5f --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.system.lwjgl; + +import com.jme3.input.lwjgl.GlfwJoystickInput; +import com.jme3.input.lwjgl.LwjglKeyInput; +import com.jme3.input.lwjgl.LwjglMouseInput; +import com.jme3.renderer.Renderer; +import com.jme3.renderer.RendererException; +import com.jme3.renderer.lwjgl.LwjglGL; +import com.jme3.renderer.lwjgl.LwjglGLExt; +import com.jme3.renderer.lwjgl.LwjglGLFboEXT; +import com.jme3.renderer.lwjgl.LwjglGLFboGL3; +import com.jme3.renderer.opengl.*; +import com.jme3.renderer.opengl.GL; +import com.jme3.system.*; +import org.lwjgl.Sys; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.*; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.lwjgl.opengl.GL.createCapabilities; +import static org.lwjgl.opengl.GL11.GL_TRUE; +import static org.lwjgl.opengl.GL11.glGetInteger; + +/** + * A LWJGL implementation of a graphics context. + */ +public abstract class LwjglContext implements JmeContext { + + private static final Logger logger = Logger.getLogger(LwjglContext.class.getName()); + + protected static final String THREAD_NAME = "jME3 Main"; + + protected AtomicBoolean created = new AtomicBoolean(false); + protected AtomicBoolean renderable = new AtomicBoolean(false); + protected final Object createdLock = new Object(); + + protected AppSettings settings = new AppSettings(true); + protected Renderer renderer; + protected LwjglKeyInput keyInput; + protected LwjglMouseInput mouseInput; + protected GlfwJoystickInput joyInput; + protected Timer timer; + protected SystemListener listener; + + public void setSystemListener(SystemListener listener) { + this.listener = listener; + } + + protected void printContextInitInfo() { + logger.log(Level.INFO, "LWJGL {0} context running on thread {1}\n" + + " * Graphics Adapter: GLFW {2}", + new Object[]{Sys.getVersion(), Thread.currentThread().getName(), GLFW.glfwGetVersionString()}); + } + + protected int determineMaxSamples() { + // If we already have a valid context, determine samples using current context. + if (GLFW.glfwExtensionSupported("GL_ARB_framebuffer_object") == GL_TRUE) { + return glGetInteger(ARBFramebufferObject.GL_MAX_SAMPLES); + } else if (GLFW.glfwExtensionSupported("GL_EXT_framebuffer_multisample") == GL_TRUE) { + return glGetInteger(EXTFramebufferMultisample.GL_MAX_SAMPLES_EXT); + } + + return Integer.MAX_VALUE; + } + + protected void registerNatives() { + // LWJGL + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/lwjgl32.dll"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/lwjgl.dll"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux32, "native/linux/liblwjgl32.so"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux64, "native/linux/liblwjgl.so"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/jemalloc32.dll"); + NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/jemalloc.dll"); + + // OpenAL + // For OSX: Need to add lib prefix when extracting + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows32, "native/windows/OpenAL32.dll"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows64, "native/windows/OpenAL.dll"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux32, "native/linux/libopenal32.so"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux64, "native/linux/libopenal.so"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX32, "native/macosx/openal.dylib", "libopenal.dylib"); + NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX64, "native/macosx/openal.dylib", "libopenal.dylib"); + } + + protected void loadNatives() { + if (JmeSystem.isLowPermissions()) { + return; + } + if ("LWJGL".equals(settings.getAudioRenderer())) { + NativeLibraryLoader.loadNativeLibrary("openal", true); + } + if (settings.useJoysticks()) { + //NativeLibraryLoader.loadNativeLibrary("jinput", true); + //NativeLibraryLoader.loadNativeLibrary("jinput-dx8", true); + } + if (NativeLibraryLoader.isUsingNativeBullet()) { + NativeLibraryLoader.loadNativeLibrary("bulletjme", true); + } + NativeLibraryLoader.loadNativeLibrary("lwjgl", true); + } + + protected int getNumSamplesToUse() { + int samples = 0; + if (settings.getSamples() > 1) { + samples = settings.getSamples(); + final int supportedSamples = determineMaxSamples(); + if (supportedSamples < samples) { + logger.log(Level.WARNING, + "Couldn't satisfy antialiasing samples requirement: x{0}. " + + "Video hardware only supports: x{1}", + new Object[]{samples, supportedSamples}); + + samples = supportedSamples; + } + } + return samples; + } + + protected void initContextFirstTime() { + final GLCapabilities capabilities = createCapabilities(settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)); + + if (!capabilities.OpenGL20) { + throw new RendererException("OpenGL 2.0 or higher is required for jMonkeyEngine"); + } + + if (settings.getRenderer().equals(AppSettings.LWJGL_OPENGL2) + || settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)) { + GL gl = new LwjglGL(); + GLExt glext = new LwjglGLExt(); + GLFbo glfbo; + + if (capabilities.OpenGL30) { + glfbo = new LwjglGLFboGL3(); + } else { + glfbo = new LwjglGLFboEXT(); + } + + if (settings.getBoolean("GraphicsDebug")) { + gl = new GLDebugDesktop(gl, glext, glfbo); + glext = (GLExt) gl; + glfbo = (GLFbo) gl; + } + + if (settings.getBoolean("GraphicsTiming")) { + GLTimingState timingState = new GLTimingState(); + gl = (GL) GLTiming.createGLTiming(gl, timingState, GL.class, GL2.class, GL3.class, GL4.class); + glext = (GLExt) GLTiming.createGLTiming(glext, timingState, GLExt.class); + glfbo = (GLFbo) GLTiming.createGLTiming(glfbo, timingState, GLFbo.class); + } + + if (settings.getBoolean("GraphicsTrace")) { + gl = (GL) GLTracer.createDesktopGlTracer(gl, GL.class, GL2.class, GL3.class, GL4.class); + glext = (GLExt) GLTracer.createDesktopGlTracer(glext, GLExt.class); + glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLFbo.class); + } + + renderer = new GLRenderer(gl, glext, glfbo); + renderer.initialize(); + } else { + throw new UnsupportedOperationException("Unsupported renderer: " + settings.getRenderer()); + } + + if (capabilities.GL_ARB_debug_output && settings.getBoolean("GraphicsDebug")) { + ARBDebugOutput.glDebugMessageCallbackARB(new LwjglGLDebugOutputHandler(), 0); // User param is zero. Not sure what we could use that for. + } + + renderer.setMainFrameBufferSrgb(settings.getGammaCorrection()); + renderer.setLinearizeSrgbImages(settings.getGammaCorrection()); + + // Init input + if (keyInput != null) { + keyInput.initialize(); + } + + if (mouseInput != null) { + mouseInput.initialize(); + } + + if (joyInput != null) { + joyInput.initialize(); + } + + renderable.set(true); + } + + public void internalDestroy() { + renderer = null; + timer = null; + renderable.set(false); + synchronized (createdLock) { + created.set(false); + createdLock.notifyAll(); + } + } + + public void internalCreate() { + synchronized (createdLock) { + created.set(true); + createdLock.notifyAll(); + } + + //if (renderable.get()) { + initContextFirstTime(); + //} else { +// assert getType() == Type.Canvas; +// } + } + + public void create() { + create(false); + } + + public void destroy() { + destroy(false); + } + + protected void waitFor(boolean createdVal) { + synchronized (createdLock) { + while (created.get() != createdVal) { + try { + createdLock.wait(); + } catch (InterruptedException ex) { + } + } + } + } + + public boolean isCreated() { + return created.get(); + } + + public boolean isRenderable() { + return renderable.get(); + } + + public void setSettings(AppSettings settings) { + this.settings.copyFrom(settings); + } + + public AppSettings getSettings() { + return settings; + } + + public Renderer getRenderer() { + return renderer; + } + + public Timer getTimer() { + return timer; + } + +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java new file mode 100644 index 000000000..4a13c30af --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java @@ -0,0 +1,12 @@ +package com.jme3.system.lwjgl; + +/** + * @author Daniel Johansson + * @since 2015-08-11 + */ +public class LwjglDisplay extends LwjglWindow { + + public LwjglDisplay() { + super(Type.Display); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java new file mode 100644 index 000000000..aa9c46511 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2009-2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.system.lwjgl; + +import org.lwjgl.opengl.ARBDebugOutput; +import org.lwjgl.opengl.GLDebugMessageARBCallback; + +import java.util.HashMap; + +class LwjglGLDebugOutputHandler extends GLDebugMessageARBCallback { + + private static final HashMap constMap = new HashMap(); + private static final String MESSAGE_FORMAT = + "[JME3] OpenGL debug message\r\n" + + " ID: %d\r\n" + + " Source: %s\r\n" + + " Type: %s\r\n" + + " Severity: %s\r\n" + + " Message: %s"; + + static { + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_API_ARB, "API"); + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_APPLICATION_ARB, "APPLICATION"); + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_OTHER_ARB, "OTHER"); + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_SHADER_COMPILER_ARB, "SHADER_COMPILER"); + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_THIRD_PARTY_ARB, "THIRD_PARTY"); + constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_WINDOW_SYSTEM_ARB, "WINDOW_SYSTEM"); + + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_ARB, "DEPRECATED_BEHAVIOR"); + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_ERROR_ARB, "ERROR"); + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_OTHER_ARB, "OTHER"); + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_PERFORMANCE_ARB, "PERFORMANCE"); + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_PORTABILITY_ARB, "PORTABILITY"); + constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_ARB, "UNDEFINED_BEHAVIOR"); + + constMap.put(ARBDebugOutput.GL_DEBUG_SEVERITY_HIGH_ARB, "HIGH"); + constMap.put(ARBDebugOutput.GL_DEBUG_SEVERITY_MEDIUM_ARB, "MEDIUM"); + constMap.put(ARBDebugOutput.GL_DEBUG_SEVERITY_LOW_ARB, "LOW"); + } + + @Override + public void invoke(int source, int type, int id, int severity, int length, long message, long userParam) { + String sourceStr = constMap.get(source); + String typeStr = constMap.get(type); + String severityStr = constMap.get(severity); + + System.err.println(String.format(MESSAGE_FORMAT, id, sourceStr, typeStr, severityStr, message)); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java new file mode 100644 index 000000000..e374e1841 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java @@ -0,0 +1,14 @@ +package com.jme3.system.lwjgl; + +import com.jme3.system.JmeContext; + +/** + * @author Daniel Johansson + * @since 2015-08-11 + */ +public class LwjglOffscreenBuffer extends LwjglWindow { + + public LwjglOffscreenBuffer() { + super(JmeContext.Type.OffscreenSurface); + } +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java new file mode 100644 index 000000000..a7960a261 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.system.lwjgl; + +import com.jme3.math.FastMath; +import com.jme3.system.Timer; +import org.lwjgl.glfw.GLFW; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Timer handles the system's time related functionality. This + * allows the calculation of the framerate. To keep the framerate calculation + * accurate, a call to update each frame is required. Timer is a + * singleton object and must be created via the getTimer method. + * + * @author Mark Powell + * @version $Id: LWJGLTimer.java,v 1.21 2007/09/22 16:46:35 irrisor Exp $ + */ +public class LwjglSmoothingTimer extends LwjglTimer { + private static final Logger logger = Logger.getLogger(LwjglSmoothingTimer.class + .getName()); + + private long lastFrameDiff; + + //frame rate parameters. + private long oldTime; + + private float lastTPF, lastFPS; + + public static int TIMER_SMOOTHNESS = 32; + + private long[] tpf; + + private int smoothIndex; + + private final static long LWJGL_TIMER_RES = 1; + private final static float INV_LWJGL_TIMER_RES = ( 1f / LWJGL_TIMER_RES ); + private static float invTimerRezSmooth; + + public final static long LWJGL_TIME_TO_NANOS = (1000000000 / LWJGL_TIMER_RES); + + private long startTime; + + private boolean allSmooth = false; + + /** + * Constructor builds a Timer object. All values will be + * initialized to it's default values. + */ + public LwjglSmoothingTimer() { + reset(); + + //print timer resolution info + logger.log(Level.FINE, "Timer resolution: {0} ticks per second", LWJGL_TIMER_RES); + } + + public void reset() { + lastFrameDiff = 0; + lastFPS = 0; + lastTPF = 0; + + // init to -1 to indicate this is a new timer. + oldTime = -1; + //reset time + startTime = (long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS); + + tpf = new long[TIMER_SMOOTHNESS]; + smoothIndex = TIMER_SMOOTHNESS - 1; + invTimerRezSmooth = ( 1f / (LWJGL_TIMER_RES * TIMER_SMOOTHNESS)); + + // set tpf... -1 values will not be used for calculating the average in update() + for ( int i = tpf.length; --i >= 0; ) { + tpf[i] = -1; + } + } + + /** + * @see Timer#getResolution() + */ + public long getResolution() { + return LWJGL_TIMER_RES; + } + + /** + * getFrameRate returns the current frame rate since the last + * call to update. + * + * @return the current frame rate. + */ + public float getFrameRate() { + return lastFPS; + } + + public float getTimePerFrame() { + return lastTPF; + } + + /** + * update recalulates the frame rate based on the previous + * call to update. It is assumed that update is called each frame. + */ + public void update() { + long newTime = (long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS); + long oldTime = this.oldTime; + this.oldTime = newTime; + if ( oldTime == -1 ) { + // For the first frame use 60 fps. This value will not be counted in further averages. + // This is done so initialization code between creating the timer and the first + // frame is not counted as a single frame on it's own. + lastTPF = 1 / 60f; + lastFPS = 1f / lastTPF; + return; + } + + long frameDiff = newTime - oldTime; + long lastFrameDiff = this.lastFrameDiff; + if ( lastFrameDiff > 0 && frameDiff > lastFrameDiff *100 ) { + frameDiff = lastFrameDiff *100; + } + this.lastFrameDiff = frameDiff; + tpf[smoothIndex] = frameDiff; + smoothIndex--; + if ( smoothIndex < 0 ) { + smoothIndex = tpf.length - 1; + } + + lastTPF = 0.0f; + if (!allSmooth) { + int smoothCount = 0; + for ( int i = tpf.length; --i >= 0; ) { + if ( tpf[i] != -1 ) { + lastTPF += tpf[i]; + smoothCount++; + } + } + if (smoothCount == tpf.length) + allSmooth = true; + lastTPF *= ( INV_LWJGL_TIMER_RES / smoothCount ); + } else { + for ( int i = tpf.length; --i >= 0; ) { + if ( tpf[i] != -1 ) { + lastTPF += tpf[i]; + } + } + lastTPF *= invTimerRezSmooth; + } + if ( lastTPF < FastMath.FLT_EPSILON ) { + lastTPF = FastMath.FLT_EPSILON; + } + + lastFPS = 1f / lastTPF; + } + + /** + * toString returns the string representation of this timer + * in the format:
+ *
+ * jme.utility.Timer@1db699b
+ * Time: {LONG}
+ * FPS: {LONG}
+ * + * @return the string representation of this object. + */ + @Override + public String toString() { + String string = super.toString(); + string += "\nTime: " + oldTime; + string += "\nFPS: " + getFrameRate(); + return string; + } +} \ No newline at end of file diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java new file mode 100644 index 000000000..c4a0e5025 --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.system.lwjgl; + +import com.jme3.system.Timer; +import org.lwjgl.glfw.GLFW; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Timer handles the system's time related functionality. This + * allows the calculation of the framerate. To keep the framerate calculation + * accurate, a call to update each frame is required. Timer is a + * singleton object and must be created via the getTimer method. + * + * @author Mark Powell + * @version $Id: LWJGLTimer.java,v 1.21 2007/09/22 16:46:35 irrisor Exp $ + */ +public class LwjglTimer extends Timer { + + private static final Logger logger = Logger.getLogger(LwjglTimer.class.getName()); + + //frame rate parameters. + private long oldTime; + private long startTime; + + private float lastTPF, lastFPS; + + private final static long LWJGL_TIMER_RES = 1; + private final static float INV_LWJGL_TIMER_RES = ( 1f / LWJGL_TIMER_RES ); + public final static long LWJGL_TIME_TO_NANOS = (1000000000 / LWJGL_TIMER_RES); + + /** + * Constructor builds a Timer object. All values will be + * initialized to it's default values. + */ + public LwjglTimer() { + reset(); + logger.log(Level.FINE, "Timer resolution: {0} ticks per second", LWJGL_TIMER_RES); + } + + public void reset() { + startTime = (long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS); + oldTime = getTime(); + } + + @Override + public float getTimeInSeconds() { + return getTime() * INV_LWJGL_TIMER_RES; + } + + /** + * @see Timer#getTime() + */ + public long getTime() { + return ((long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS) - startTime); + } + + /** + * @see Timer#getResolution() + */ + public long getResolution() { + return LWJGL_TIMER_RES; + } + + /** + * getFrameRate returns the current frame rate since the last + * call to update. + * + * @return the current frame rate. + */ + public float getFrameRate() { + return lastFPS; + } + + public float getTimePerFrame() { + return lastTPF; + } + + /** + * update recalulates the frame rate based on the previous + * call to update. It is assumed that update is called each frame. + */ + public void update() { + long curTime = getTime(); + lastTPF = (curTime - oldTime) * (1.0f / LWJGL_TIMER_RES); + lastFPS = 1.0f / lastTPF; + oldTime = curTime; + } + + /** + * toString returns the string representation of this timer + * in the format:
+ *
+ * jme.utility.Timer@1db699b
+ * Time: {LONG}
+ * FPS: {LONG}
+ * + * @return the string representation of this object. + */ + @Override + public String toString() { + String string = super.toString(); + string += "\nTime: " + oldTime; + string += "\nFPS: " + getFrameRate(); + return string; + } +} \ No newline at end of file diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java new file mode 100644 index 000000000..c089bf28d --- /dev/null +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java @@ -0,0 +1,489 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.system.lwjgl; + +import com.jme3.input.JoyInput; +import com.jme3.input.KeyInput; +import com.jme3.input.MouseInput; +import com.jme3.input.TouchInput; +import com.jme3.input.lwjgl.GlfwJoystickInput; +import com.jme3.input.lwjgl.LwjglKeyInput; +import com.jme3.input.lwjgl.LwjglMouseInput; +import com.jme3.system.AppSettings; +import com.jme3.system.JmeContext; +import com.jme3.system.JmeSystem; +import org.lwjgl.PointerBuffer; +import org.lwjgl.Sys; +import org.lwjgl.glfw.*; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL11.GL_FALSE; +import static org.lwjgl.opengl.GL11.GL_TRUE; +import static org.lwjgl.system.MemoryUtil.NULL; + +/** + * A wrapper class over the GLFW framework in LWJGL 3. + * + * @author Daniel Johansson (dannyjo) + * @since 3.1 + */ +public abstract class LwjglWindow extends LwjglContext implements Runnable { + + private static final Logger LOGGER = Logger.getLogger(LwjglWindow.class.getName()); + + protected AtomicBoolean needClose = new AtomicBoolean(false); + protected final AtomicBoolean needRestart = new AtomicBoolean(false); + protected boolean wasActive = false; + protected boolean autoFlush = true; + protected boolean allowSwapBuffers = false; + private long window = -1; + private final JmeContext.Type type; + + private GLFWErrorCallback errorCallback; + private GLFWWindowSizeCallback windowSizeCallback; + private GLFWWindowFocusCallback windowFocusCallback; + + public LwjglWindow(final JmeContext.Type type) { + if (!JmeContext.Type.Display.equals(type) && !JmeContext.Type.OffscreenSurface.equals(type) && !JmeContext.Type.Canvas.equals(type)) { + throw new IllegalArgumentException("Unsupported type '" + type.name() + "' provided"); + } + + this.type = type; + } + + /** + * @return Type.Display or Type.Canvas + */ + public JmeContext.Type getType() { + return type; + } + + /** + * Set the title if its a windowed display + * + * @param title + */ + public void setTitle(final String title) { + if (created.get() && window != -1) { + glfwSetWindowTitle(window, title); + } + } + + /** + * Restart if its a windowed or full-screen display. + */ + public void restart() { + if (created.get()) { + needRestart.set(true); + } else { + LOGGER.warning("Display is not created, cannot restart window."); + } + } + + /** + * Apply the settings, changing resolution, etc. + * + * @param settings + */ + protected void createContext(final AppSettings settings) { + glfwSetErrorCallback(errorCallback = new GLFWErrorCallback() { + @Override + public void invoke(int error, long description) { + final String message = Callbacks.errorCallbackDescriptionString(description); + listener.handleError(message, new Exception(message)); + } + }); + + if (glfwInit() != GL_TRUE) { + throw new IllegalStateException("Unable to initialize GLFW"); + } + + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_VISIBLE, GL_FALSE); + + // TODO: Add support for monitor selection + long monitor = NULL; + + if (settings.isFullscreen()) { + monitor = glfwGetPrimaryMonitor(); + } + + final ByteBuffer videoMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + + if (settings.getWidth() <= 0 || settings.getHeight() <= 0) { + settings.setResolution(GLFWvidmode.width(videoMode), GLFWvidmode.height(videoMode)); + } + + window = glfwCreateWindow(settings.getWidth(), settings.getHeight(), settings.getTitle(), monitor, NULL); + + if (window == NULL) { + throw new RuntimeException("Failed to create the GLFW window"); + } + + glfwWindowHint(GLFW_RESIZABLE, settings.isResizable() ? GL_TRUE : GL_FALSE); + glfwWindowHint(GLFW_DEPTH_BITS, settings.getDepthBits()); + glfwWindowHint(GLFW_STENCIL_BITS, settings.getStencilBits()); + glfwWindowHint(GLFW_SAMPLES, settings.getSamples()); + glfwWindowHint(GLFW_STEREO, settings.useStereo3D() ? GL_TRUE : GL_FALSE); + + int frameRateCap = settings.getFrameRate(); + + if (!autoFlush) { + frameRateCap = 20; + } + + if (frameRateCap > 0) { + glfwWindowHint(GLFW_REFRESH_RATE, frameRateCap); + } + + // Not sure how else to support bits per pixel + if (settings.getBitsPerPixel() == 24) { + glfwWindowHint(GLFW_RED_BITS, 8); + glfwWindowHint(GLFW_GREEN_BITS, 8); + glfwWindowHint(GLFW_BLUE_BITS, 8); + } else if (settings.getBitsPerPixel() == 16) { + glfwWindowHint(GLFW_RED_BITS, 5); + glfwWindowHint(GLFW_GREEN_BITS, 6); + glfwWindowHint(GLFW_BLUE_BITS, 5); + } + + glfwWindowHint(GLFW_ALPHA_BITS, settings.getAlphaBits()); + + glfwSetWindowFocusCallback(window, windowFocusCallback = new GLFWWindowFocusCallback() { + @Override + public void invoke(final long window, final int focused) { + final boolean focus = (focused == 1); + + if (wasActive != focus) { + if (!wasActive) { + listener.gainFocus(); + timer.reset(); + } else { + listener.loseFocus(); + } + + wasActive = !wasActive; + } + + } + }); + + // Center the window + if (!settings.isFullscreen() && Type.Display.equals(type)) { + glfwSetWindowPos(window, (GLFWvidmode.width(videoMode) - settings.getWidth()) / 2, (GLFWvidmode.height(videoMode) - settings.getHeight()) / 2); + } + + // Make the OpenGL context current + glfwMakeContextCurrent(window); + + // Enable vsync + if (settings.isVSync()) { + glfwSwapInterval(1); + } else { + glfwSwapInterval(0); + } + + + // Make the window visible + if (Type.Display.equals(type)) { + glfwShowWindow(window); + } + + // Add a resize callback which delegates to the listener + glfwSetWindowSizeCallback(window, windowSizeCallback = new GLFWWindowSizeCallback() { + @Override + public void invoke(final long window, final int width, final int height) { + settings.setResolution(width, height); + listener.reshape(width, height); + } + }); + + allowSwapBuffers = settings.isSwapBuffers(); + + // TODO: When GLFW 3.2 is released and included in LWJGL 3.x then we should hopefully be able to set the window icon. + } + + /** + * Destroy the context. + */ + protected void destroyContext() { + try { + if (renderer != null) { + renderer.cleanup(); + } + + errorCallback.release(); + windowSizeCallback.release(); + windowFocusCallback.release(); + + if (window != 0) { + glfwDestroyWindow(window); + } + //glfwTerminate(); + } catch (Exception ex) { + listener.handleError("Failed to destroy context", ex); + } + } + + public void create(boolean waitFor) { + if (created.get()) { + LOGGER.warning("create() called when display is already created!"); + return; + } + + new Thread(this, THREAD_NAME).start(); + if (waitFor) + waitFor(true); + } + + /** + * Does LWJGL display initialization in the OpenGL thread + */ + protected boolean initInThread() { + try { + if (!JmeSystem.isLowPermissions()) { + // Enable uncaught exception handler only for current thread + Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + public void uncaughtException(Thread thread, Throwable thrown) { + listener.handleError("Uncaught exception thrown in " + thread.toString(), thrown); + if (needClose.get()) { + // listener.handleError() has requested the + // context to close. Satisfy request. + deinitInThread(); + } + } + }); + } + + timer = new LwjglTimer(); + + // For canvas, this will create a pbuffer, + // allowing us to query information. + // When the canvas context becomes available, it will + // be replaced seamlessly. + createContext(settings); + printContextInitInfo(); + + created.set(true); + super.internalCreate(); + } catch (Exception ex) { + try { + if (window != -1) { + //glfwSetWindowShouldClose(window, GL_TRUE); + glfwDestroyWindow(window); + } + } catch (Exception ex2) { + LOGGER.log(Level.WARNING, null, ex2); + } + + listener.handleError("Failed to create display", ex); + return false; // if we failed to create display, do not continue + } + + listener.initialize(); + return true; + } + + /** + * execute one iteration of the render loop in the OpenGL thread + */ + protected void runLoop() { + // If a restart is required, lets recreate the context. + if (needRestart.getAndSet(false)) { + try { + createContext(settings); + } catch (Exception ex) { + LOGGER.log(Level.SEVERE, "Failed to set display settings!", ex); + } + + LOGGER.fine("Display restarted."); + } + + if (!created.get()) { + throw new IllegalStateException(); + } + + listener.update(); + + // All this does is call swap buffers + // If the canvas is not active, there's no need to waste time + // doing that .. + if (renderable.get()) { + // calls swap buffers, etc. + try { + if (allowSwapBuffers && autoFlush) { + glfwSwapBuffers(window); + } + } catch (Throwable ex) { + listener.handleError("Error while swapping buffers", ex); + } + } + + if (glfwGetWindowAttrib(window, GLFW_FOCUSED) == GL_TRUE) { + glfwPollEvents(); + } + + // Subclasses just call GLObjectManager clean up objects here + // it is safe .. for now. + if (renderer != null) { + renderer.postFrame(); + } + } + + /** + * De-initialize in the OpenGL thread. + */ + protected void deinitInThread() { + destroyContext(); + + listener.destroy(); + LOGGER.fine("Display destroyed."); + super.internalDestroy(); + } + + public void run() { + if (listener == null) { + throw new IllegalStateException("SystemListener is not set on context!" + + "Must set with JmeContext.setSystemListner()."); + } + + registerNatives(); + loadNatives(); + LOGGER.log(Level.FINE, "Using LWJGL {0}", Sys.getVersion()); + + if (!initInThread()) { + LOGGER.log(Level.SEVERE, "Display initialization failed. Cannot continue."); + return; + } + + while (true) { + if (glfwWindowShouldClose(window) == GL_TRUE) { + listener.requestClose(false); + } + + runLoop(); + + if (needClose.get()) { + break; + } + } + + deinitInThread(); + } + + public JoyInput getJoyInput() { + if (joyInput == null) { + joyInput = new GlfwJoystickInput(); + } + return joyInput; + } + + public MouseInput getMouseInput() { + if (mouseInput == null) { + mouseInput = new LwjglMouseInput(this); + } + return mouseInput; + } + + public KeyInput getKeyInput() { + if (keyInput == null) { + keyInput = new LwjglKeyInput(this); + } + + return keyInput; + } + + public TouchInput getTouchInput() { + return null; + } + + public void setAutoFlushFrames(boolean enabled) { + this.autoFlush = enabled; + } + + public void destroy(boolean waitFor) { + needClose.set(true); + + if (waitFor) { + waitFor(false); + } + } + + public long getWindowHandle() { + return window; + } + + private ByteBuffer[] imagesToByteBuffers(Object[] images) { + ByteBuffer[] out = new ByteBuffer[images.length]; + for (int i = 0; i < images.length; i++) { + BufferedImage image = (BufferedImage) images[i]; + out[i] = imageToByteBuffer(image); + } + return out; + } + + private ByteBuffer imageToByteBuffer(BufferedImage image) { + if (image.getType() != BufferedImage.TYPE_INT_ARGB_PRE) { + BufferedImage convertedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE); + Graphics2D g = convertedImage.createGraphics(); + double width = image.getWidth() * (double) 1; + double height = image.getHeight() * (double) 1; + g.drawImage(image, (int) ((convertedImage.getWidth() - width) / 2), + (int) ((convertedImage.getHeight() - height) / 2), + (int) (width), (int) (height), null); + g.dispose(); + image = convertedImage; + } + + byte[] imageBuffer = new byte[image.getWidth() * image.getHeight() * 4]; + int counter = 0; + for (int i = 0; i < image.getHeight(); i++) { + for (int j = 0; j < image.getWidth(); j++) { + int colorSpace = image.getRGB(j, i); + imageBuffer[counter + 0] = (byte) ((colorSpace << 8) >> 24); + imageBuffer[counter + 1] = (byte) ((colorSpace << 16) >> 24); + imageBuffer[counter + 2] = (byte) ((colorSpace << 24) >> 24); + imageBuffer[counter + 3] = (byte) (colorSpace >> 24); + counter += 4; + } + } + return ByteBuffer.wrap(imageBuffer); + } +} diff --git a/settings.gradle b/settings.gradle index 89b8fc075..8b9000340 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,6 +15,7 @@ include 'jme3-desktop' include 'jme3-blender' include 'jme3-jogl' include 'jme3-lwjgl' +include 'jme3-lwjgl3' // Other external dependencies include 'jme3-jbullet' From 2f6388c13b13e95baca3f330e1195074bac6c0a6 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sat, 29 Aug 2015 12:32:54 -0400 Subject: [PATCH 11/94] travis-ci: fix build failures on PRs / branches --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bdc1336f7..fb7588262 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ install: script: - ./gradlew check - ./gradlew createZipDistribution - - "[ $TRAVIS_BRANCH == 'master' ] && [ $TRAVIS_PULL_REQUEST == 'false' ] && ./gradlew uploadArchives;" + - [ $TRAVIS_BRANCH == 'master' ] && [ $TRAVIS_PULL_REQUEST == 'false' ] && ./gradlew uploadArchives || : before_deploy: - export RELEASE_DIST=$(ls build/distributions/*.zip) From 814fb2b3ff90a8c72909e6d180487994faf9054a Mon Sep 17 00:00:00 2001 From: Julien Gouesse Date: Sat, 29 Aug 2015 21:27:20 +0200 Subject: [PATCH 12/94] First implementation of the unified renderer for the JOGL backend, untested --- .../java/com/jme3/renderer/jogl/JoglGL.java | 594 ++++++++++++++++++ .../com/jme3/renderer/jogl/JoglGLExt.java | 88 +++ .../com/jme3/renderer/jogl/JoglGLFbo.java | 97 +++ .../jme3/system/jogl/JoglAbstractDisplay.java | 5 +- .../java/com/jme3/system/jogl/JoglCanvas.java | 33 +- .../com/jme3/system/jogl/JoglContext.java | 114 +++- .../system/jogl/JoglGLDebugOutputHandler.java | 80 +++ .../system/jogl/JoglNewtAbstractDisplay.java | 5 +- .../com/jme3/system/jogl/JoglNewtCanvas.java | 34 +- 9 files changed, 1015 insertions(+), 35 deletions(-) create mode 100644 jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL.java create mode 100644 jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLExt.java create mode 100644 jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLFbo.java create mode 100644 jme3-jogl/src/main/java/com/jme3/system/jogl/JoglGLDebugOutputHandler.java diff --git a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL.java b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL.java new file mode 100644 index 000000000..6f335e960 --- /dev/null +++ b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL.java @@ -0,0 +1,594 @@ +package com.jme3.renderer.jogl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GL; +import com.jme3.renderer.opengl.GL2; +import com.jme3.renderer.opengl.GL3; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; + +import com.jme3.renderer.opengl.GL4; +import com.jogamp.opengl.GLContext; + +public class JoglGL implements GL, GL2, GL3, GL4 { + + private static int getLimitBytes(ByteBuffer buffer) { + checkLimit(buffer); + return buffer.limit(); + } + + private static int getLimitBytes(ShortBuffer buffer) { + checkLimit(buffer); + return buffer.limit() * 2; + } + + private static int getLimitBytes(FloatBuffer buffer) { + checkLimit(buffer); + return buffer.limit() * 4; + } + + private static int getLimitCount(Buffer buffer, int elementSize) { + checkLimit(buffer); + return buffer.limit() / elementSize; + } + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + @Override + public void resetStats() { + } + + @Override + public void glActiveTexture(int param1) { + GLContext.getCurrentGL().glActiveTexture(param1); + } + + @Override + public void glAlphaFunc(int param1, float param2) { + GLContext.getCurrentGL().getGL2ES1().glAlphaFunc(param1, param2); + } + + @Override + public void glAttachShader(int param1, int param2) { + GLContext.getCurrentGL().getGL2ES2().glAttachShader(param1, param2); + } + + @Override + public void glBindBuffer(int param1, int param2) { + GLContext.getCurrentGL().glBindBuffer(param1, param2); + } + + @Override + public void glBindTexture(int param1, int param2) { + GLContext.getCurrentGL().glBindTexture(param1, param2); + } + + @Override + public void glBlendFunc(int param1, int param2) { + GLContext.getCurrentGL().glBlendFunc(param1, param2); + } + + @Override + public void glBufferData(int param1, long param2, int param3) { + GLContext.getCurrentGL().glBufferData(param1, param2, null, param3); + } + + @Override + public void glBufferData(int param1, FloatBuffer param2, int param3) { + checkLimit(param2); + GLContext.getCurrentGL().glBufferData(param1, getLimitBytes(param2), param2, param3); + } + + @Override + public void glBufferData(int param1, ShortBuffer param2, int param3) { + checkLimit(param2); + GLContext.getCurrentGL().glBufferData(param1, getLimitBytes(param2), param2, param3); + } + + @Override + public void glBufferData(int param1, ByteBuffer param2, int param3) { + checkLimit(param2); + GLContext.getCurrentGL().glBufferData(param1, getLimitBytes(param2), param2, param3); + } + + @Override + public void glBufferSubData(int param1, long param2, FloatBuffer param3) { + checkLimit(param3); + GLContext.getCurrentGL().glBufferSubData(param1, param2, getLimitBytes(param3), param3); + } + + @Override + public void glBufferSubData(int param1, long param2, ShortBuffer param3) { + checkLimit(param3); + GLContext.getCurrentGL().glBufferSubData(param1, param2, getLimitBytes(param3), param3); + } + + @Override + public void glBufferSubData(int param1, long param2, ByteBuffer param3) { + checkLimit(param3); + GLContext.getCurrentGL().glBufferSubData(param1, param2, getLimitBytes(param3), param3); + } + + @Override + public void glClear(int param1) { + GLContext.getCurrentGL().glClear(param1); + } + + @Override + public void glClearColor(float param1, float param2, float param3, float param4) { + GLContext.getCurrentGL().glClearColor(param1, param2, param3, param4); + } + + @Override + public void glColorMask(boolean param1, boolean param2, boolean param3, boolean param4) { + GLContext.getCurrentGL().glColorMask(param1, param2, param3, param4); + } + + @Override + public void glCompileShader(int param1) { + GLContext.getCurrentGL().getGL2ES2().glCompileShader(param1); + } + + @Override + public void glCompressedTexImage2D(int param1, int param2, int param3, int param4, int param5, int param6, ByteBuffer param7) { + checkLimit(param7); + GLContext.getCurrentGL().glCompressedTexImage2D(param1, param2, param3, param4, param5, param6, getLimitBytes(param7), param7); + } + + @Override + public void glCompressedTexImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, ByteBuffer param8) { + checkLimit(param8); + GLContext.getCurrentGL().getGL2ES2().glCompressedTexImage3D(param1, param2, param3, param4, param5, param6, param7, getLimitBytes(param8), param8); + } + + @Override + public void glCompressedTexSubImage2D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, ByteBuffer param8) { + checkLimit(param8); + GLContext.getCurrentGL().glCompressedTexSubImage2D(param1, param2, param3, param4, param5, param6, param7, getLimitBytes(param8), param8); + } + + @Override + public void glCompressedTexSubImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, int param9, ByteBuffer param10) { + checkLimit(param10); + GLContext.getCurrentGL().getGL2ES2().glCompressedTexSubImage3D(param1, param2, param3, param4, param5, param6, param7, param8, param9, getLimitBytes(param10), param10); + } + + @Override + public int glCreateProgram() { + return GLContext.getCurrentGL().getGL2ES2().glCreateProgram(); + } + + @Override + public int glCreateShader(int param1) { + return GLContext.getCurrentGL().getGL2ES2().glCreateShader(param1); + } + + @Override + public void glCullFace(int param1) { + GLContext.getCurrentGL().glCullFace(param1); + } + + @Override + public void glDeleteBuffers(IntBuffer param1) { + checkLimit(param1); + GLContext.getCurrentGL().glDeleteBuffers(param1.limit(), param1); + } + + @Override + public void glDeleteProgram(int param1) { + GLContext.getCurrentGL().getGL2ES2().glDeleteProgram(param1); + } + + @Override + public void glDeleteShader(int param1) { + GLContext.getCurrentGL().getGL2ES2().glDeleteShader(param1); + } + + @Override + public void glDeleteTextures(IntBuffer param1) { + checkLimit(param1); + GLContext.getCurrentGL().glDeleteTextures(param1.limit() ,param1); + } + + @Override + public void glDepthFunc(int param1) { + GLContext.getCurrentGL().glDepthFunc(param1); + } + + @Override + public void glDepthMask(boolean param1) { + GLContext.getCurrentGL().glDepthMask(param1); + } + + @Override + public void glDepthRange(double param1, double param2) { + GLContext.getCurrentGL().glDepthRange(param1, param2); + } + + @Override + public void glDetachShader(int param1, int param2) { + GLContext.getCurrentGL().getGL2ES2().glDetachShader(param1, param2); + } + + @Override + public void glDisable(int param1) { + GLContext.getCurrentGL().glDisable(param1); + } + + @Override + public void glDisableVertexAttribArray(int param1) { + GLContext.getCurrentGL().getGL2ES2().glDisableVertexAttribArray(param1); + } + + @Override + public void glDrawArrays(int param1, int param2, int param3) { + GLContext.getCurrentGL().glDrawArrays(param1, param2, param3); + } + + @Override + public void glDrawBuffer(int param1) { + GLContext.getCurrentGL().getGL2GL3().glDrawBuffer(param1); + } + + @Override + public void glDrawRangeElements(int param1, int param2, int param3, int param4, int param5, long param6) { + GLContext.getCurrentGL().getGL2ES3().glDrawRangeElements(param1, param2, param3, param4, param5, param6); + } + + @Override + public void glEnable(int param1) { + GLContext.getCurrentGL().glEnable(param1); + } + + @Override + public void glEnableVertexAttribArray(int param1) { + GLContext.getCurrentGL().getGL2ES2().glEnableVertexAttribArray(param1); + } + + @Override + public void glGenBuffers(IntBuffer param1) { + checkLimit(param1); + GLContext.getCurrentGL().glGenBuffers(param1.limit(), param1); + } + + @Override + public void glGenTextures(IntBuffer param1) { + checkLimit(param1); + GLContext.getCurrentGL().glGenTextures(param1.limit(), param1); + } + + @Override + public void glGetBoolean(int param1, ByteBuffer param2) { + checkLimit(param2); + GLContext.getCurrentGL().glGetBooleanv(param1, param2); + } + + @Override + public void glGetBufferSubData(int target, long offset, ByteBuffer data) { + checkLimit(data); + GLContext.getCurrentGL().getGL2GL3().glGetBufferSubData(target, offset, getLimitBytes(data), data); + } + + @Override + public int glGetError() { + return GLContext.getCurrentGL().glGetError(); + } + + @Override + public void glGetInteger(int param1, IntBuffer param2) { + checkLimit(param2); + GLContext.getCurrentGL().glGetIntegerv(param1, param2); + } + + @Override + public void glGetProgram(int param1, int param2, IntBuffer param3) { + checkLimit(param3); + GLContext.getCurrentGL().getGL2ES2().glGetProgramiv(param1, param2, param3); + } + + @Override + public void glGetShader(int param1, int param2, IntBuffer param3) { + checkLimit(param3); + GLContext.getCurrentGL().getGL2ES2().glGetShaderiv(param1, param2, param3); + } + + @Override + public String glGetString(int param1) { + return GLContext.getCurrentGL().glGetString(param1); + } + + @Override + public String glGetString(int param1, int param2) { + return GLContext.getCurrentGL().getGL2ES3().glGetStringi(param1, param2); + } + + @Override + public boolean glIsEnabled(int param1) { + return GLContext.getCurrentGL().glIsEnabled(param1); + } + + @Override + public void glLineWidth(float param1) { + GLContext.getCurrentGL().glLineWidth(param1); + } + + @Override + public void glLinkProgram(int param1) { + GLContext.getCurrentGL().getGL2ES2().glLinkProgram(param1); + } + + @Override + public void glPixelStorei(int param1, int param2) { + GLContext.getCurrentGL().glPixelStorei(param1, param2); + } + + @Override + public void glPointSize(float param1) { + GLContext.getCurrentGL().getGL2ES1().glPointSize(param1); + } + + @Override + public void glPolygonMode(int param1, int param2) { + GLContext.getCurrentGL().getGL2().glPolygonMode(param1, param2); + } + + @Override + public void glPolygonOffset(float param1, float param2) { + GLContext.getCurrentGL().glPolygonOffset(param1, param2); + } + + @Override + public void glReadBuffer(int param1) { + GLContext.getCurrentGL().getGL2ES3().glReadBuffer(param1); + } + + @Override + public void glReadPixels(int param1, int param2, int param3, int param4, int param5, int param6, ByteBuffer param7) { + checkLimit(param7); + GLContext.getCurrentGL().glReadPixels(param1, param2, param3, param4, param5, param6, param7); + } + + @Override + public void glReadPixels(int param1, int param2, int param3, int param4, int param5, int param6, long param7) { + GLContext.getCurrentGL().glReadPixels(param1, param2, param3, param4, param5, param6, param7); + } + + @Override + public void glScissor(int param1, int param2, int param3, int param4) { + GLContext.getCurrentGL().glScissor(param1, param2, param3, param4); + } + + @Override + public void glStencilFuncSeparate(int param1, int param2, int param3, int param4) { + GLContext.getCurrentGL().getGL2ES2().glStencilFuncSeparate(param1, param2, param3, param4); + } + + @Override + public void glStencilOpSeparate(int param1, int param2, int param3, int param4) { + GLContext.getCurrentGL().getGL2ES2().glStencilOpSeparate(param1, param2, param3, param4); + } + + @Override + public void glTexImage2D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, ByteBuffer param9) { + checkLimit(param9); + GLContext.getCurrentGL().glTexImage2D(param1, param2, param3, param4, param5, param6, param7, param8, param9); + } + + @Override + public void glTexImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, int param9, ByteBuffer param10) { + checkLimit(param10); + GLContext.getCurrentGL().getGL2ES2().glTexImage3D(param1, param2, param3, param4, param5, param6, param7, param8, param9, param10); + } + + @Override + public void glTexParameterf(int param1, int param2, float param3) { + GLContext.getCurrentGL().glTexParameterf(param1, param2, param3); + } + + @Override + public void glTexParameteri(int param1, int param2, int param3) { + GLContext.getCurrentGL().glTexParameteri(param1, param2, param3); + } + + @Override + public void glTexSubImage2D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, ByteBuffer param9) { + checkLimit(param9); + GLContext.getCurrentGL().glTexSubImage2D(param1, param2, param3, param4, param5, param6, param7, param8, param9); + } + + @Override + public void glTexSubImage3D(int param1, int param2, int param3, int param4, int param5, int param6, int param7, int param8, int param9, int param10, ByteBuffer param11) { + checkLimit(param11); + GLContext.getCurrentGL().getGL2ES2().glTexSubImage3D(param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11); + } + + @Override + public void glUniform1(int param1, FloatBuffer param2) { + checkLimit(param2); + GLContext.getCurrentGL().getGL2ES2().glUniform1fv(param1, getLimitCount(param2, 1), param2); + } + + @Override + public void glUniform1(int param1, IntBuffer param2) { + checkLimit(param2); + GLContext.getCurrentGL().getGL2ES2().glUniform1iv(param1, getLimitCount(param2, 1), param2); + } + + @Override + public void glUniform1f(int param1, float param2) { + GLContext.getCurrentGL().getGL2ES2().glUniform1f(param1, param2); + } + + @Override + public void glUniform1i(int param1, int param2) { + GLContext.getCurrentGL().getGL2ES2().glUniform1i(param1, param2); + } + + @Override + public void glUniform2(int param1, IntBuffer param2) { + checkLimit(param2); + GLContext.getCurrentGL().getGL2ES2().glUniform2iv(param1, getLimitCount(param2, 2), param2); + } + + @Override + public void glUniform2(int param1, FloatBuffer param2) { + checkLimit(param2); + GLContext.getCurrentGL().getGL2ES2().glUniform2fv(param1, getLimitCount(param2, 2), param2); + } + + @Override + public void glUniform2f(int param1, float param2, float param3) { + GLContext.getCurrentGL().getGL2ES2().glUniform2f(param1, param2, param3); + } + + @Override + public void glUniform3(int param1, IntBuffer param2) { + checkLimit(param2); + GLContext.getCurrentGL().getGL2ES2().glUniform3iv(param1, getLimitCount(param2, 3), param2); + } + + @Override + public void glUniform3(int param1, FloatBuffer param2) { + checkLimit(param2); + GLContext.getCurrentGL().getGL2ES2().glUniform3fv(param1, getLimitCount(param2, 3), param2); + } + + @Override + public void glUniform3f(int param1, float param2, float param3, float param4) { + GLContext.getCurrentGL().getGL2ES2().glUniform3f(param1, param2, param3, param4); + } + + @Override + public void glUniform4(int param1, FloatBuffer param2) { + checkLimit(param2); + GLContext.getCurrentGL().getGL2ES2().glUniform4fv(param1, getLimitCount(param2, 4), param2); + } + + @Override + public void glUniform4(int param1, IntBuffer param2) { + checkLimit(param2); + GLContext.getCurrentGL().getGL2ES2().glUniform4iv(param1, getLimitCount(param2, 4), param2); + } + + @Override + public void glUniform4f(int param1, float param2, float param3, float param4, float param5) { + GLContext.getCurrentGL().getGL2ES2().glUniform4f(param1, param2, param3, param4, param5); + } + + @Override + public void glUniformMatrix3(int param1, boolean param2, FloatBuffer param3) { + checkLimit(param3); + GLContext.getCurrentGL().getGL2ES2().glUniformMatrix3fv(param1, getLimitCount(param3, 3 * 3), param2, param3); + } + + @Override + public void glUniformMatrix4(int param1, boolean param2, FloatBuffer param3) { + checkLimit(param3); + GLContext.getCurrentGL().getGL2ES2().glUniformMatrix4fv(param1, getLimitCount(param3, 4 * 4), param2, param3); + } + + @Override + public void glUseProgram(int param1) { + GLContext.getCurrentGL().getGL2ES2().glUseProgram(param1); + } + + @Override + public void glVertexAttribPointer(int param1, int param2, int param3, boolean param4, int param5, long param6) { + GLContext.getCurrentGL().getGL2ES2().glVertexAttribPointer(param1, param2, param3, param4, param5, param6); + } + + @Override + public void glViewport(int param1, int param2, int param3, int param4) { + GLContext.getCurrentGL().glViewport(param1, param2, param3, param4); + } + + @Override + public int glGetAttribLocation(int param1, String param2) { + // FIXME: Does JOGL require null-terminated strings????? + return GLContext.getCurrentGL().getGL2ES2().glGetAttribLocation(param1, param2 + "\0"); + } + + @Override + public int glGetUniformLocation(int param1, String param2) { + // FIXME: Does JOGL require null-terminated strings???????? + return GLContext.getCurrentGL().getGL2ES2().glGetUniformLocation(param1, param2 + "\0"); + } + + @Override + public void glShaderSource(int param1, String[] param2, IntBuffer param3) { + checkLimit(param3); + GLContext.getCurrentGL().getGL2ES2().glShaderSource(param1, param2.length, param2, param3); + } + + @Override + public String glGetProgramInfoLog(int program, int maxSize) { + ByteBuffer buffer = ByteBuffer.allocateDirect(maxSize); + buffer.order(ByteOrder.nativeOrder()); + ByteBuffer tmp = ByteBuffer.allocateDirect(4); + tmp.order(ByteOrder.nativeOrder()); + IntBuffer intBuffer = tmp.asIntBuffer(); + + GLContext.getCurrentGL().getGL2ES2().glGetProgramInfoLog(program, maxSize, intBuffer, buffer); + int numBytes = intBuffer.get(0); + byte[] bytes = new byte[numBytes]; + buffer.get(bytes); + return new String(bytes); + } + + @Override + public String glGetShaderInfoLog(int shader, int maxSize) { + ByteBuffer buffer = ByteBuffer.allocateDirect(maxSize); + buffer.order(ByteOrder.nativeOrder()); + ByteBuffer tmp = ByteBuffer.allocateDirect(4); + tmp.order(ByteOrder.nativeOrder()); + IntBuffer intBuffer = tmp.asIntBuffer(); + + GLContext.getCurrentGL().getGL2ES2().glGetShaderInfoLog(shader, maxSize, intBuffer, buffer); + int numBytes = intBuffer.get(0); + byte[] bytes = new byte[numBytes]; + buffer.get(bytes); + return new String(bytes); + } + + @Override + public void glBindFragDataLocation(int param1, int param2, String param3) { + GLContext.getCurrentGL().getGL2GL3().glBindFragDataLocation(param1, param2, param3); + } + + @Override + public void glBindVertexArray(int param1) { + GLContext.getCurrentGL().getGL2ES3().glBindVertexArray(param1); + } + + @Override + public void glGenVertexArrays(IntBuffer param1) { + checkLimit(param1); + GLContext.getCurrentGL().getGL2ES3().glGenVertexArrays(param1.limit(), param1); + } + + @Override + public void glPatchParameter(int count) { + GLContext.getCurrentGL().getGL3().glPatchParameteri(com.jogamp.opengl.GL3.GL_PATCH_VERTICES, count); + } + + @Override + public void glDeleteVertexArrays(IntBuffer arrays) { + checkLimit(arrays); + GLContext.getCurrentGL().getGL2ES3().glDeleteVertexArrays(arrays.limit(), arrays); + } +} diff --git a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLExt.java b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLExt.java new file mode 100644 index 000000000..2171388b9 --- /dev/null +++ b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLExt.java @@ -0,0 +1,88 @@ +package com.jme3.renderer.jogl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GLExt; +import com.jogamp.opengl.GLContext; + +import java.nio.Buffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public class JoglGLExt implements GLExt { + + private static int getLimitBytes(IntBuffer buffer) { + checkLimit(buffer); + return buffer.limit() * 4; + } + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + @Override + public void glBufferData(int target, IntBuffer data, int usage) { + checkLimit(data); + GLContext.getCurrentGL().glBufferData(target, getLimitBytes(data), data, usage); + } + + @Override + public void glBufferSubData(int target, long offset, IntBuffer data) { + checkLimit(data); + GLContext.getCurrentGL().glBufferSubData(target, getLimitBytes(data), offset, data); + } + + @Override + public void glDrawArraysInstancedARB(int mode, int first, int count, int primcount) { + GLContext.getCurrentGL().getGL2ES3().glDrawArraysInstanced(mode, first, count, primcount); + } + + @Override + public void glDrawBuffers(IntBuffer bufs) { + checkLimit(bufs); + GLContext.getCurrentGL().getGL2ES2().glDrawBuffers(bufs.limit(), bufs); + } + + @Override + public void glDrawElementsInstancedARB(int mode, int indices_count, int type, long indices_buffer_offset, int primcount) { + GLContext.getCurrentGL().getGL2ES3().glDrawElementsInstanced(mode, indices_count, type, indices_buffer_offset, primcount); + } + + @Override + public void glGetMultisample(int pname, int index, FloatBuffer val) { + checkLimit(val); + GLContext.getCurrentGL().getGL2ES2().glGetMultisamplefv(pname, index, val); + } + + @Override + public void glTexImage2DMultisample(int target, int samples, int internalformat, int width, int height, boolean fixedsamplelocations) { + GLContext.getCurrentGL().getGL2ES2().glTexImage2DMultisample(target, samples, internalformat, width, height, fixedsamplelocations); + } + + @Override + public void glVertexAttribDivisorARB(int index, int divisor) { + GLContext.getCurrentGL().getGL2ES3().glVertexAttribDivisor(index, divisor); + } + + @Override + public Object glFenceSync(int condition, int flags) { + return GLContext.getCurrentGL().getGL3ES3().glFenceSync(condition, flags); + } + + @Override + public int glClientWaitSync(Object sync, int flags, long timeout) { + return GLContext.getCurrentGL().getGL3ES3().glClientWaitSync((long) sync, flags, timeout); + } + + @Override + public void glDeleteSync(Object sync) { + GLContext.getCurrentGL().getGL3ES3().glDeleteSync((long) sync); + } +} diff --git a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLFbo.java b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLFbo.java new file mode 100644 index 000000000..2691d2e24 --- /dev/null +++ b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLFbo.java @@ -0,0 +1,97 @@ +package com.jme3.renderer.jogl; + +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.GLFbo; +import com.jogamp.opengl.GLContext; + +import java.nio.Buffer; +import java.nio.IntBuffer; + +/** + * Implements GLFbo + * + * @author Kirill Vainer + */ +public class JoglGLFbo implements GLFbo { + + private static void checkLimit(Buffer buffer) { + if (buffer == null) { + return; + } + if (buffer.limit() == 0) { + throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error"); + } + if (buffer.remaining() == 0) { + throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error"); + } + } + + @Override + public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) { + GLContext.getCurrentGL().getGL2ES3().glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } + + @Override + public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height) { + GLContext.getCurrentGL().glRenderbufferStorageMultisample(target, samples, internalformat, width, height); + } + + @Override + public void glBindFramebufferEXT(int param1, int param2) { + GLContext.getCurrentGL().glBindFramebuffer(param1, param2); + } + + @Override + public void glBindRenderbufferEXT(int param1, int param2) { + GLContext.getCurrentGL().glBindRenderbuffer(param1, param2); + } + + @Override + public int glCheckFramebufferStatusEXT(int param1) { + return GLContext.getCurrentGL().glCheckFramebufferStatus(param1); + } + + @Override + public void glDeleteFramebuffersEXT(IntBuffer param1) { + checkLimit(param1); + GLContext.getCurrentGL().glDeleteFramebuffers(param1.limit(), param1); + } + + @Override + public void glDeleteRenderbuffersEXT(IntBuffer param1) { + checkLimit(param1); + GLContext.getCurrentGL().glDeleteRenderbuffers(param1.limit(), param1); + } + + @Override + public void glFramebufferRenderbufferEXT(int param1, int param2, int param3, int param4) { + GLContext.getCurrentGL().glFramebufferRenderbuffer(param1, param2, param3, param4); + } + + @Override + public void glFramebufferTexture2DEXT(int param1, int param2, int param3, int param4, int param5) { + GLContext.getCurrentGL().glFramebufferTexture2D(param1, param2, param3, param4, param5); + } + + @Override + public void glGenFramebuffersEXT(IntBuffer param1) { + checkLimit(param1); + GLContext.getCurrentGL().glGenFramebuffers(param1.limit(), param1); + } + + @Override + public void glGenRenderbuffersEXT(IntBuffer param1) { + checkLimit(param1); + GLContext.getCurrentGL().glGenRenderbuffers(param1.limit(), param1); + } + + @Override + public void glGenerateMipmapEXT(int param1) { + GLContext.getCurrentGL().glGenerateMipmap(param1); + } + + @Override + public void glRenderbufferStorageEXT(int param1, int param2, int param3, int param4) { + GLContext.getCurrentGL().glRenderbufferStorage(param1, param2, param3, param4); + } +} diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java index 2248e4b22..f1cbfbee6 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java @@ -128,7 +128,8 @@ public abstract class JoglAbstractDisplay extends JoglContext implements GLEvent canvas.setIgnoreRepaint(true); canvas.addGLEventListener(this); - if (settings.getBoolean("GraphicsDebug")) { + //TODO remove this block once for all when the unified renderer is stable + /*if (settings.getBoolean("GraphicsDebug")) { canvas.invoke(false, new GLRunnable() { public boolean run(GLAutoDrawable glad) { GL gl = glad.getGL(); @@ -177,7 +178,7 @@ public abstract class JoglAbstractDisplay extends JoglContext implements GLEvent renderer.setMainFrameBufferSrgb(settings.getGammaCorrection()); return true; } - }); + });*/ } protected void startGLCanvas() { diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglCanvas.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglCanvas.java index 9a409b85e..b56eb43c1 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglCanvas.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglCanvas.java @@ -41,28 +41,34 @@ public class JoglCanvas extends JoglAbstractDisplay implements JmeCanvasContext private static final Logger logger = Logger.getLogger(JoglCanvas.class.getName()); private int width, height; + private boolean runningFirstTime = true; public JoglCanvas(){ super(); initGLCanvas(); } - public Type getType() { + @Override + public Type getType() { return Type.Canvas; } - public void setTitle(String title) { + @Override + public void setTitle(String title) { } - public void restart() { + @Override + public void restart() { } - public void create(boolean waitFor){ + @Override + public void create(boolean waitFor){ if (waitFor) waitFor(true); } - public void destroy(boolean waitFor){ + @Override + public void destroy(boolean waitFor){ if (waitFor) waitFor(false); if (animator.isAnimating()) @@ -81,13 +87,20 @@ public class JoglCanvas extends JoglAbstractDisplay implements JmeCanvasContext startGLCanvas(); } - public void init(GLAutoDrawable drawable) { + @Override + public void init(GLAutoDrawable drawable) { canvas.requestFocus(); super.internalCreate(); logger.fine("Display created."); - renderer.initialize(); + // At this point, the OpenGL context is active. + if (runningFirstTime){ + // THIS is the part that creates the renderer. + // It must always be called, now that we have the pbuffer workaround. + initContextFirstTime(); + runningFirstTime = false; + } listener.initialize(); } @@ -97,7 +110,8 @@ public class JoglCanvas extends JoglAbstractDisplay implements JmeCanvasContext super.startGLCanvas(); } - public void display(GLAutoDrawable glad) { + @Override + public void display(GLAutoDrawable glad) { if (!created.get() && renderer != null){ listener.destroy(); logger.fine("Canvas destroyed."); @@ -129,7 +143,8 @@ public class JoglCanvas extends JoglAbstractDisplay implements JmeCanvasContext } - public Canvas getCanvas() { + @Override + public Canvas getCanvas() { return canvas; } diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java index 5d687af8d..25ad4fc8e 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java @@ -36,17 +36,32 @@ import com.jme3.input.JoyInput; import com.jme3.input.KeyInput; import com.jme3.input.MouseInput; import com.jme3.renderer.Renderer; -import com.jme3.renderer.jogl.JoglRenderer; +import com.jme3.renderer.RendererException; +import com.jme3.renderer.jogl.JoglGL; +import com.jme3.renderer.jogl.JoglGLExt; +import com.jme3.renderer.jogl.JoglGLFbo; +import com.jme3.renderer.opengl.GL2; +import com.jme3.renderer.opengl.GL3; +import com.jme3.renderer.opengl.GL4; +import com.jme3.renderer.opengl.GLDebugDesktop; +import com.jme3.renderer.opengl.GLExt; +import com.jme3.renderer.opengl.GLFbo; +import com.jme3.renderer.opengl.GLRenderer; +import com.jme3.renderer.opengl.GLTiming; +import com.jme3.renderer.opengl.GLTimingState; +import com.jme3.renderer.opengl.GLTracer; import com.jme3.system.AppSettings; import com.jme3.system.JmeContext; import com.jme3.system.NanoTimer; import com.jme3.system.NativeLibraryLoader; import com.jme3.system.SystemListener; import com.jme3.system.Timer; + import java.nio.IntBuffer; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; + import com.jogamp.opengl.GL; import com.jogamp.opengl.GL2GL3; import com.jogamp.opengl.GLContext; @@ -62,7 +77,7 @@ public abstract class JoglContext implements JmeContext { protected final Object createdLock = new Object(); protected AppSettings settings = new AppSettings(true); - protected JoglRenderer renderer; + protected Renderer renderer; protected Timer timer; protected SystemListener listener; @@ -77,43 +92,53 @@ public abstract class JoglContext implements JmeContext { } } - public void setSystemListener(SystemListener listener){ + @Override + public void setSystemListener(SystemListener listener){ this.listener = listener; } - public void setSettings(AppSettings settings) { + @Override + public void setSettings(AppSettings settings) { this.settings.copyFrom(settings); } - public boolean isRenderable(){ + @Override + public boolean isRenderable(){ return renderable.get(); } - public AppSettings getSettings() { + @Override + public AppSettings getSettings() { return settings; } - public Renderer getRenderer() { + @Override + public Renderer getRenderer() { return renderer; } - public MouseInput getMouseInput() { + @Override + public MouseInput getMouseInput() { return mouseInput; } - public KeyInput getKeyInput() { + @Override + public KeyInput getKeyInput() { return keyInput; } - public JoyInput getJoyInput() { + @Override + public JoyInput getJoyInput() { return joyInput; } - public Timer getTimer() { + @Override + public Timer getTimer() { return timer; } - public boolean isCreated() { + @Override + public boolean isCreated() { return created.get(); } @@ -135,13 +160,76 @@ public abstract class JoglContext implements JmeContext { } } } + + protected void initContextFirstTime(){ + if (GLContext.getCurrent().getGLVersionNumber().getMajor() < 2) { + throw new RendererException("OpenGL 2.0 or higher is " + + "required for jMonkeyEngine"); + } + + if (settings.getRenderer().equals("JOGL")) { + com.jme3.renderer.opengl.GL gl = new JoglGL(); + GLExt glext = new JoglGLExt(); + GLFbo glfbo = new JoglGLFbo(); + + if (settings.getBoolean("GraphicsDebug")) { + gl = new GLDebugDesktop(gl, glext, glfbo); + glext = (GLExt) gl; + glfbo = (GLFbo) gl; + } + + if (settings.getBoolean("GraphicsTiming")) { + GLTimingState timingState = new GLTimingState(); + gl = (com.jme3.renderer.opengl.GL) GLTiming.createGLTiming(gl, timingState, GL.class, GL2.class, GL3.class, GL4.class); + glext = (GLExt) GLTiming.createGLTiming(glext, timingState, GLExt.class); + glfbo = (GLFbo) GLTiming.createGLTiming(glfbo, timingState, GLFbo.class); + } + + if (settings.getBoolean("GraphicsTrace")) { + gl = (com.jme3.renderer.opengl.GL) GLTracer.createDesktopGlTracer(gl, GL.class, GL2.class, GL3.class, GL4.class); + glext = (GLExt) GLTracer.createDesktopGlTracer(glext, GLExt.class); + glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLFbo.class); + } + + renderer = new GLRenderer(gl, glext, glfbo); + renderer.initialize(); + } else { + throw new UnsupportedOperationException("Unsupported renderer: " + settings.getRenderer()); + } + + if (GLContext.getCurrentGL().isExtensionAvailable("GL_ARB_debug_output") && settings.getBoolean("GraphicsDebug")) { + GLContext.getCurrent().enableGLDebugMessage(true); + GLContext.getCurrent().addGLDebugListener(new JoglGLDebugOutputHandler()); + } + + renderer.setMainFrameBufferSrgb(settings.getGammaCorrection()); + renderer.setLinearizeSrgbImages(settings.getGammaCorrection()); - public void internalCreate() { + // Init input + if (keyInput != null) { + keyInput.initialize(); + } + + if (mouseInput != null) { + mouseInput.initialize(); + } + + if (joyInput != null) { + joyInput.initialize(); + } + } + + public void internalCreate() { timer = new NanoTimer(); synchronized (createdLock){ created.set(true); createdLock.notifyAll(); } + if (renderable.get()){ + initContextFirstTime(); + } else { + assert getType() == Type.Canvas; + } } protected void internalDestroy() { diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglGLDebugOutputHandler.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglGLDebugOutputHandler.java new file mode 100644 index 000000000..5a8b58715 --- /dev/null +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglGLDebugOutputHandler.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2009-2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.system.jogl; + +import java.util.HashMap; + +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLDebugListener; +import com.jogamp.opengl.GLDebugMessage; + +class JoglGLDebugOutputHandler implements GLDebugListener { + + private static final HashMap constMap = new HashMap(); + private static final String MESSAGE_FORMAT = + "[JME3] OpenGL debug message\r\n" + + " ID: %d\r\n" + + " Source: %s\r\n" + + " Type: %s\r\n" + + " Severity: %s\r\n" + + " Message: %s"; + + static { + constMap.put(GL2ES2.GL_DEBUG_SOURCE_API, "API"); + constMap.put(GL2ES2.GL_DEBUG_SOURCE_APPLICATION, "APPLICATION"); + constMap.put(GL2ES2.GL_DEBUG_SOURCE_OTHER, "OTHER"); + constMap.put(GL2ES2.GL_DEBUG_SOURCE_SHADER_COMPILER, "SHADER_COMPILER"); + constMap.put(GL2ES2.GL_DEBUG_SOURCE_THIRD_PARTY, "THIRD_PARTY"); + constMap.put(GL2ES2.GL_DEBUG_SOURCE_WINDOW_SYSTEM, "WINDOW_SYSTEM"); + + constMap.put(GL2ES2.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR, "DEPRECATED_BEHAVIOR"); + constMap.put(GL2ES2.GL_DEBUG_TYPE_ERROR, "ERROR"); + constMap.put(GL2ES2.GL_DEBUG_TYPE_OTHER, "OTHER"); + constMap.put(GL2ES2.GL_DEBUG_TYPE_PERFORMANCE, "PERFORMANCE"); + constMap.put(GL2ES2.GL_DEBUG_TYPE_PORTABILITY, "PORTABILITY"); + constMap.put(GL2ES2.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR, "UNDEFINED_BEHAVIOR"); + + constMap.put(GL2ES2.GL_DEBUG_SEVERITY_HIGH, "HIGH"); + constMap.put(GL2ES2.GL_DEBUG_SEVERITY_MEDIUM, "MEDIUM"); + constMap.put(GL2ES2.GL_DEBUG_SEVERITY_LOW, "LOW"); + } + + @Override + public void messageSent(GLDebugMessage event) { + String sourceStr = constMap.get(event.getDbgSource()); + String typeStr = constMap.get(event.getDbgType()); + String severityStr = constMap.get(event.getDbgSeverity()); + + System.err.println(String.format(MESSAGE_FORMAT, event.getDbgId(), sourceStr, typeStr, severityStr, event.getDbgMsg())); + } + +} diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java index f05c92b95..99e322970 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java @@ -107,7 +107,8 @@ public abstract class JoglNewtAbstractDisplay extends JoglContext implements GLE canvas.setSize(settings.getWidth(), settings.getHeight()); canvas.addGLEventListener(this); - if (settings.getBoolean("GraphicsDebug")) { + //TODO remove this block once for all when the unified renderer is stable + /*if (settings.getBoolean("GraphicsDebug")) { canvas.invoke(false, new GLRunnable() { public boolean run(GLAutoDrawable glad) { GL gl = glad.getGL(); @@ -156,7 +157,7 @@ public abstract class JoglNewtAbstractDisplay extends JoglContext implements GLE renderer.setMainFrameBufferSrgb(settings.getGammaCorrection()); return true; } - }); + });*/ } protected void startGLCanvas() { diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtCanvas.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtCanvas.java index 3ed501580..e4e81a5df 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtCanvas.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtCanvas.java @@ -41,6 +41,7 @@ public class JoglNewtCanvas extends JoglNewtAbstractDisplay implements JmeCanvas private static final Logger logger = Logger.getLogger(JoglNewtCanvas.class.getName()); private int width, height; + private boolean runningFirstTime = true; private NewtCanvasAWT newtAwtCanvas; @@ -53,7 +54,9 @@ public class JoglNewtCanvas extends JoglNewtAbstractDisplay implements JmeCanvas protected final void initGLCanvas() { super.initGLCanvas(); newtAwtCanvas = new NewtCanvasAWT(canvas) { - @Override + private static final long serialVersionUID = 1L; + + @Override public void addNotify() { super.addNotify(); onCanvasAdded(); @@ -67,22 +70,27 @@ public class JoglNewtCanvas extends JoglNewtAbstractDisplay implements JmeCanvas }; } - public Type getType() { + @Override + public Type getType() { return Type.Canvas; } - public void setTitle(String title) { + @Override + public void setTitle(String title) { } - public void restart() { + @Override + public void restart() { } - public void create(boolean waitFor){ + @Override + public void create(boolean waitFor){ if (waitFor) waitFor(true); } - public void destroy(boolean waitFor){ + @Override + public void destroy(boolean waitFor){ if (waitFor) waitFor(false); if (animator.isAnimating()) @@ -101,13 +109,20 @@ public class JoglNewtCanvas extends JoglNewtAbstractDisplay implements JmeCanvas startGLCanvas(); } - public void init(GLAutoDrawable drawable) { + @Override + public void init(GLAutoDrawable drawable) { canvas.requestFocus(); super.internalCreate(); logger.fine("Display created."); - renderer.initialize(); + // At this point, the OpenGL context is active. + if (runningFirstTime){ + // THIS is the part that creates the renderer. + // It must always be called, now that we have the pbuffer workaround. + initContextFirstTime(); + runningFirstTime = false; + } listener.initialize(); } @@ -117,7 +132,8 @@ public class JoglNewtCanvas extends JoglNewtAbstractDisplay implements JmeCanvas super.startGLCanvas(); } - public void display(GLAutoDrawable glad) { + @Override + public void display(GLAutoDrawable glad) { if (!created.get() && renderer != null){ listener.destroy(); logger.fine("Canvas destroyed."); From 2631684b3db6b0bc969aa1dbab719069fb6b72d6 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sat, 29 Aug 2015 21:21:34 -0400 Subject: [PATCH 13/94] sdk build: don't upload empty jars to maven from sdk project --- sdk/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/build.gradle b/sdk/build.gradle index d1d71b8fc..99c850987 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -418,7 +418,7 @@ task cleanSdk() <<{ file("JME3TestsTemplateAndroid/src/jmetest/").deleteDir() } -jar.dependsOn(buildSdk) -clean.dependsOn(cleanSdk); - +tasks.remove(uploadArchives) +jar.dependsOn(buildSdk) +clean.dependsOn(cleanSdk) From fae50fd36e9f6bc06c4f437119fbc354748b53c4 Mon Sep 17 00:00:00 2001 From: Julien Gouesse Date: Sun, 30 Aug 2015 21:36:58 +0200 Subject: [PATCH 14/94] Fixes a compile error in the unified renderer of the JOGL backend, doesn't rely on the auto-boxing --- jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLExt.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLExt.java b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLExt.java index 2171388b9..a87e8a035 100644 --- a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLExt.java +++ b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGLExt.java @@ -78,11 +78,11 @@ public class JoglGLExt implements GLExt { @Override public int glClientWaitSync(Object sync, int flags, long timeout) { - return GLContext.getCurrentGL().getGL3ES3().glClientWaitSync((long) sync, flags, timeout); + return GLContext.getCurrentGL().getGL3ES3().glClientWaitSync(((Long) sync).longValue(), flags, timeout); } @Override public void glDeleteSync(Object sync) { - GLContext.getCurrentGL().getGL3ES3().glDeleteSync((long) sync); + GLContext.getCurrentGL().getGL3ES3().glDeleteSync(((Long) sync).longValue()); } } From 7bd414665b4837564001232413cd19d149117150 Mon Sep 17 00:00:00 2001 From: Julien Gouesse Date: Sun, 30 Aug 2015 21:47:39 +0200 Subject: [PATCH 15/94] Drives the display renderable earlier in order to fix the unified renderer of the JOGL backend --- .../main/java/com/jme3/system/jogl/JoglAbstractDisplay.java | 6 +++--- .../java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java index f1cbfbee6..7126eeb64 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java @@ -127,6 +127,9 @@ public abstract class JoglAbstractDisplay extends JoglContext implements GLEvent canvas.setSize(settings.getWidth(), settings.getHeight()); canvas.setIgnoreRepaint(true); canvas.addGLEventListener(this); + + //FIXME not sure it is the best place to do that + renderable.set(true); //TODO remove this block once for all when the unified renderer is stable /*if (settings.getBoolean("GraphicsDebug")) { @@ -193,9 +196,6 @@ public abstract class JoglAbstractDisplay extends JoglContext implements GLEvent animator.start(); wasAnimating = true; - - //FIXME not sure it is the best place to do that - renderable.set(true); } protected void onCanvasAdded() { diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java index 99e322970..bf068b1a9 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java @@ -106,6 +106,9 @@ public abstract class JoglNewtAbstractDisplay extends JoglContext implements GLE canvas.requestFocus(); canvas.setSize(settings.getWidth(), settings.getHeight()); canvas.addGLEventListener(this); + + //FIXME not sure it is the best place to do that + renderable.set(true); //TODO remove this block once for all when the unified renderer is stable /*if (settings.getBoolean("GraphicsDebug")) { @@ -172,9 +175,6 @@ public abstract class JoglNewtAbstractDisplay extends JoglContext implements GLE animator.start(); wasAnimating = true; - - //FIXME not sure it is the best place to do that - renderable.set(true); } protected void onCanvasAdded() { From fcae2e64deff492fdee52487295c21e7aff497e8 Mon Sep 17 00:00:00 2001 From: Julien Gouesse Date: Mon, 31 Aug 2015 23:15:09 +0200 Subject: [PATCH 16/94] Removes some useless code from the JogAmp backend --- .../jme3/system/jogl/JoglAbstractDisplay.java | 62 ------------------- .../system/jogl/JoglNewtAbstractDisplay.java | 62 ------------------- 2 files changed, 124 deletions(-) diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java index 7126eeb64..7b3f2e655 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java @@ -37,7 +37,6 @@ import com.jme3.input.MouseInput; import com.jme3.input.TouchInput; import com.jme3.input.awt.AwtKeyInput; import com.jme3.input.awt.AwtMouseInput; -import com.jme3.renderer.jogl.JoglRenderer; import com.jogamp.opengl.util.Animator; import com.jogamp.opengl.util.AnimatorBase; import com.jogamp.opengl.util.FPSAnimator; @@ -47,15 +46,6 @@ import java.awt.GraphicsEnvironment; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; -import com.jogamp.opengl.DebugGL2; -import com.jogamp.opengl.DebugGL3; -import com.jogamp.opengl.DebugGL3bc; -import com.jogamp.opengl.DebugGL4; -import com.jogamp.opengl.DebugGL4bc; -import com.jogamp.opengl.DebugGLES1; -import com.jogamp.opengl.DebugGLES2; -import com.jogamp.opengl.DebugGLES3; -import com.jogamp.opengl.GL; import com.jogamp.opengl.GLAutoDrawable; import com.jogamp.opengl.GLCapabilities; import com.jogamp.opengl.GLEventListener; @@ -130,58 +120,6 @@ public abstract class JoglAbstractDisplay extends JoglContext implements GLEvent //FIXME not sure it is the best place to do that renderable.set(true); - - //TODO remove this block once for all when the unified renderer is stable - /*if (settings.getBoolean("GraphicsDebug")) { - canvas.invoke(false, new GLRunnable() { - public boolean run(GLAutoDrawable glad) { - GL gl = glad.getGL(); - if (gl.isGLES()) { - if (gl.isGLES1()) { - glad.setGL(new DebugGLES1(gl.getGLES1())); - } else { - if (gl.isGLES2()) { - glad.setGL(new DebugGLES2(gl.getGLES2())); - } else { - if (gl.isGLES3()) { - glad.setGL(new DebugGLES3(gl.getGLES3())); - } - } - } - } else { - if (gl.isGL4bc()) { - glad.setGL(new DebugGL4bc(gl.getGL4bc())); - } else { - if (gl.isGL4()) { - glad.setGL(new DebugGL4(gl.getGL4())); - } else { - if (gl.isGL3bc()) { - glad.setGL(new DebugGL3bc(gl.getGL3bc())); - } else { - if (gl.isGL3()) { - glad.setGL(new DebugGL3(gl.getGL3())); - } else { - if (gl.isGL2()) { - glad.setGL(new DebugGL2(gl.getGL2())); - } - } - } - } - } - } - return true; - } - }); - } - - renderer = new JoglRenderer(); - - canvas.invoke(false, new GLRunnable() { - public boolean run(GLAutoDrawable glad) { - renderer.setMainFrameBufferSrgb(settings.getGammaCorrection()); - return true; - } - });*/ } protected void startGLCanvas() { diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java index bf068b1a9..4ac6f5c49 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java @@ -37,7 +37,6 @@ import com.jme3.input.MouseInput; import com.jme3.input.TouchInput; import com.jme3.input.jogl.NewtKeyInput; import com.jme3.input.jogl.NewtMouseInput; -import com.jme3.renderer.jogl.JoglRenderer; import com.jogamp.newt.opengl.GLWindow; import com.jogamp.opengl.util.Animator; import com.jogamp.opengl.util.AnimatorBase; @@ -46,15 +45,6 @@ import com.jogamp.opengl.util.FPSAnimator; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; -import com.jogamp.opengl.DebugGL2; -import com.jogamp.opengl.DebugGL3; -import com.jogamp.opengl.DebugGL3bc; -import com.jogamp.opengl.DebugGL4; -import com.jogamp.opengl.DebugGL4bc; -import com.jogamp.opengl.DebugGLES1; -import com.jogamp.opengl.DebugGLES2; -import com.jogamp.opengl.DebugGLES3; -import com.jogamp.opengl.GL; import com.jogamp.opengl.GLAutoDrawable; import com.jogamp.opengl.GLCapabilities; import com.jogamp.opengl.GLEventListener; @@ -109,58 +99,6 @@ public abstract class JoglNewtAbstractDisplay extends JoglContext implements GLE //FIXME not sure it is the best place to do that renderable.set(true); - - //TODO remove this block once for all when the unified renderer is stable - /*if (settings.getBoolean("GraphicsDebug")) { - canvas.invoke(false, new GLRunnable() { - public boolean run(GLAutoDrawable glad) { - GL gl = glad.getGL(); - if (gl.isGLES()) { - if (gl.isGLES1()) { - glad.setGL(new DebugGLES1(gl.getGLES1())); - } else { - if (gl.isGLES2()) { - glad.setGL(new DebugGLES2(gl.getGLES2())); - } else { - if (gl.isGLES3()) { - glad.setGL(new DebugGLES3(gl.getGLES3())); - } - } - } - } else { - if (gl.isGL4bc()) { - glad.setGL(new DebugGL4bc(gl.getGL4bc())); - } else { - if (gl.isGL4()) { - glad.setGL(new DebugGL4(gl.getGL4())); - } else { - if (gl.isGL3bc()) { - glad.setGL(new DebugGL3bc(gl.getGL3bc())); - } else { - if (gl.isGL3()) { - glad.setGL(new DebugGL3(gl.getGL3())); - } else { - if (gl.isGL2()) { - glad.setGL(new DebugGL2(gl.getGL2())); - } - } - } - } - } - } - return true; - } - }); - } - - renderer = new JoglRenderer(); - - canvas.invoke(false, new GLRunnable() { - public boolean run(GLAutoDrawable glad) { - renderer.setMainFrameBufferSrgb(settings.getGammaCorrection()); - return true; - } - });*/ } protected void startGLCanvas() { From da01826e438e30e2591204168ae0ac90e61624fc Mon Sep 17 00:00:00 2001 From: Julien Gouesse Date: Mon, 31 Aug 2015 23:17:48 +0200 Subject: [PATCH 17/94] Temporarily disables the unified renderer in the JogAmp backend --- .../src/main/java/com/jme3/system/jogl/JoglContext.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java index 25ad4fc8e..3b90877df 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java @@ -40,6 +40,7 @@ import com.jme3.renderer.RendererException; import com.jme3.renderer.jogl.JoglGL; import com.jme3.renderer.jogl.JoglGLExt; import com.jme3.renderer.jogl.JoglGLFbo; +import com.jme3.renderer.jogl.JoglRenderer; import com.jme3.renderer.opengl.GL2; import com.jme3.renderer.opengl.GL3; import com.jme3.renderer.opengl.GL4; @@ -191,7 +192,9 @@ public abstract class JoglContext implements JmeContext { glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLFbo.class); } - renderer = new GLRenderer(gl, glext, glfbo); + //FIXME uncomment the line below when the unified renderer is ready for the prime time :) + //renderer = new GLRenderer(gl, glext, glfbo); + renderer = new JoglRenderer(); renderer.initialize(); } else { throw new UnsupportedOperationException("Unsupported renderer: " + settings.getRenderer()); From 2677ff31b9307dd0268a1f68841f10685e9af6d5 Mon Sep 17 00:00:00 2001 From: Julien Gouesse Date: Mon, 31 Aug 2015 23:21:19 +0200 Subject: [PATCH 18/94] Adds the JogAmp backend into the Gradle dependencies --- jme3-examples/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/jme3-examples/build.gradle b/jme3-examples/build.gradle index 9ddb9cf78..09e71997e 100644 --- a/jme3-examples/build.gradle +++ b/jme3-examples/build.gradle @@ -21,6 +21,7 @@ dependencies { // compile project(':jme3-bullet-native') compile project(':jme3-jbullet') compile project(':jme3-jogg') + compile project(':jme3-jogl') compile project(':jme3-lwjgl') compile project(':jme3-networking') compile project(':jme3-niftygui') From 971b9524bd436966f35ee8f284115dd975622c50 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Wed, 2 Sep 2015 12:47:21 +0100 Subject: [PATCH 19/94] Fixed an issue with the GLFW mouse input coordinates not being mapped correctly. Renamed input classes to GlfwXXX. --- .../{LwjglKeyInput.java => GlfwKeyInput.java} | 6 +-- ...jglMouseInput.java => GlfwMouseInput.java} | 41 +++++++++++++++---- .../com/jme3/system/lwjgl/LwjglContext.java | 21 ++++------ .../com/jme3/system/lwjgl/LwjglWindow.java | 14 +++---- 4 files changed, 52 insertions(+), 30 deletions(-) rename jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/{LwjglKeyInput.java => GlfwKeyInput.java} (95%) rename jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/{LwjglMouseInput.java => GlfwMouseInput.java} (80%) diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java similarity index 95% rename from jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java rename to jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java index a39010c95..4768290d7 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglKeyInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java @@ -45,9 +45,9 @@ import java.util.logging.Logger; import static org.lwjgl.glfw.GLFW.*; -public class LwjglKeyInput implements KeyInput { +public class GlfwKeyInput implements KeyInput { - private static final Logger logger = Logger.getLogger(LwjglKeyInput.class.getName()); + private static final Logger logger = Logger.getLogger(GlfwKeyInput.class.getName()); private LwjglWindow context; private RawInputListener listener; @@ -55,7 +55,7 @@ public class LwjglKeyInput implements KeyInput { private GLFWKeyCallback keyCallback; private Queue keyInputEvents = new LinkedList(); - public LwjglKeyInput(LwjglWindow context) { + public GlfwKeyInput(LwjglWindow context) { this.context = context; } diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwMouseInput.java similarity index 80% rename from jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java rename to jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwMouseInput.java index 45262ffbc..41e76a29d 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/LwjglMouseInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwMouseInput.java @@ -50,9 +50,17 @@ import java.util.logging.Logger; import static org.lwjgl.glfw.GLFW.*; -public class LwjglMouseInput implements MouseInput { +/** + * Captures mouse input using GLFW callbacks. It then temporarily stores these in event queues which are processed in the + * {@link #update()} method. Due to some of the GLFW button id's there is a conversion method in this class which will + * convert the GLFW left, middle and right mouse button to JME3 left, middle and right button codes. + * + * @author Daniel Johansson (dannyjo) + * @since 3.1 + */ +public class GlfwMouseInput implements MouseInput { - private static final Logger logger = Logger.getLogger(LwjglMouseInput.class.getName()); + private static final Logger logger = Logger.getLogger(GlfwMouseInput.class.getName()); private LwjglWindow context; private RawInputListener listener; @@ -67,7 +75,7 @@ public class LwjglMouseInput implements MouseInput { private Queue mouseMotionEvents = new LinkedList(); private Queue mouseButtonEvents = new LinkedList(); - public LwjglMouseInput(LwjglWindow context) { + public GlfwMouseInput(final LwjglWindow context) { this.context = context; } @@ -78,7 +86,7 @@ public class LwjglMouseInput implements MouseInput { int xDelta; int yDelta; int x = (int) Math.round(xpos); - int y = (int) Math.round(ypos); + int y = context.getSettings().getHeight() - (int) Math.round(ypos); if (mouseX == 0) { mouseX = x; @@ -94,7 +102,7 @@ public class LwjglMouseInput implements MouseInput { mouseY = y; if (xDelta != 0 || yDelta != 0) { - final MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(x, y * -1, xDelta, yDelta * -1, mouseWheel, 0); + final MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(x, y, xDelta, yDelta, mouseWheel, 0); mouseMotionEvent.setTime(getInputTimeNanos()); mouseMotionEvents.add(mouseMotionEvent); } @@ -115,7 +123,7 @@ public class LwjglMouseInput implements MouseInput { glfwSetMouseButtonCallback(context.getWindowHandle(), mouseButtonCallback = new GLFWMouseButtonCallback() { @Override public void invoke(final long window, final int button, final int action, final int mods) { - final MouseButtonEvent mouseButtonEvent = new MouseButtonEvent(button, action == GLFW_PRESS, mouseX, mouseY); + final MouseButtonEvent mouseButtonEvent = new MouseButtonEvent(convertButton(button), action == GLFW_PRESS, mouseX, mouseY); mouseButtonEvent.setTime(getInputTimeNanos()); mouseButtonEvents.add(mouseButtonEvent); } @@ -131,7 +139,7 @@ public class LwjglMouseInput implements MouseInput { } public int getButtonCount() { - return 2; // TODO: How to determine this? + return GLFW_MOUSE_BUTTON_LAST + 1; } public void update() { @@ -186,4 +194,23 @@ public class LwjglMouseInput implements MouseInput { glfwSetCursor(context.getWindowHandle(), cursor); } } + + /** + * Simply converts the GLFW button code to a JME button code. If there is no match it just returns the GLFW button + * code. Bare in mind GLFW supports 8 different mouse buttons. + * + * @param glfwButton the raw GLFW button index. + * @return the mapped {@link MouseInput} button id. + */ + private int convertButton(final int glfwButton) { + if (glfwButton == GLFW_MOUSE_BUTTON_LEFT) { + return MouseInput.BUTTON_LEFT; + } else if(glfwButton == GLFW_MOUSE_BUTTON_MIDDLE) { + return MouseInput.BUTTON_MIDDLE; + } else if(glfwButton == GLFW_MOUSE_BUTTON_RIGHT) { + return MouseInput.BUTTON_RIGHT; + } + + return glfwButton; + } } diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java index 3f6491e5f..86b6a294a 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -33,8 +33,8 @@ package com.jme3.system.lwjgl; import com.jme3.input.lwjgl.GlfwJoystickInput; -import com.jme3.input.lwjgl.LwjglKeyInput; -import com.jme3.input.lwjgl.LwjglMouseInput; +import com.jme3.input.lwjgl.GlfwKeyInput; +import com.jme3.input.lwjgl.GlfwMouseInput; import com.jme3.renderer.Renderer; import com.jme3.renderer.RendererException; import com.jme3.renderer.lwjgl.LwjglGL; @@ -71,8 +71,8 @@ public abstract class LwjglContext implements JmeContext { protected AppSettings settings = new AppSettings(true); protected Renderer renderer; - protected LwjglKeyInput keyInput; - protected LwjglMouseInput mouseInput; + protected GlfwKeyInput keyInput; + protected GlfwMouseInput mouseInput; protected GlfwJoystickInput joyInput; protected Timer timer; protected SystemListener listener; @@ -123,16 +123,15 @@ public abstract class LwjglContext implements JmeContext { if (JmeSystem.isLowPermissions()) { return; } + if ("LWJGL".equals(settings.getAudioRenderer())) { NativeLibraryLoader.loadNativeLibrary("openal", true); } - if (settings.useJoysticks()) { - //NativeLibraryLoader.loadNativeLibrary("jinput", true); - //NativeLibraryLoader.loadNativeLibrary("jinput-dx8", true); - } + if (NativeLibraryLoader.isUsingNativeBullet()) { NativeLibraryLoader.loadNativeLibrary("bulletjme", true); } + NativeLibraryLoader.loadNativeLibrary("lwjgl", true); } @@ -236,11 +235,7 @@ public abstract class LwjglContext implements JmeContext { createdLock.notifyAll(); } - //if (renderable.get()) { - initContextFirstTime(); - //} else { -// assert getType() == Type.Canvas; -// } + initContextFirstTime(); } public void create() { diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java index c089bf28d..4315618b1 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java @@ -37,12 +37,11 @@ import com.jme3.input.KeyInput; import com.jme3.input.MouseInput; import com.jme3.input.TouchInput; import com.jme3.input.lwjgl.GlfwJoystickInput; -import com.jme3.input.lwjgl.LwjglKeyInput; -import com.jme3.input.lwjgl.LwjglMouseInput; +import com.jme3.input.lwjgl.GlfwKeyInput; +import com.jme3.input.lwjgl.GlfwMouseInput; import com.jme3.system.AppSettings; import com.jme3.system.JmeContext; import com.jme3.system.JmeSystem; -import org.lwjgl.PointerBuffer; import org.lwjgl.Sys; import org.lwjgl.glfw.*; @@ -256,7 +255,6 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { if (window != 0) { glfwDestroyWindow(window); } - //glfwTerminate(); } catch (Exception ex) { listener.handleError("Failed to destroy context", ex); } @@ -269,8 +267,10 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { } new Thread(this, THREAD_NAME).start(); - if (waitFor) + + if (waitFor) { waitFor(true); + } } /** @@ -417,14 +417,14 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { public MouseInput getMouseInput() { if (mouseInput == null) { - mouseInput = new LwjglMouseInput(this); + mouseInput = new GlfwMouseInput(this); } return mouseInput; } public KeyInput getKeyInput() { if (keyInput == null) { - keyInput = new LwjglKeyInput(this); + keyInput = new GlfwKeyInput(this); } return keyInput; From 66e0e7053c9ddd947ab63e2bd9c4ae572aedc4db Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 3 Sep 2015 13:31:25 +0100 Subject: [PATCH 20/94] Fixed an issue where GLFW would not accept window focus again due to event polling not running. --- .../main/java/com/jme3/system/lwjgl/LwjglWindow.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java index 4315618b1..6eac34cdc 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java @@ -188,7 +188,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { glfwSetWindowFocusCallback(window, windowFocusCallback = new GLFWWindowFocusCallback() { @Override public void invoke(final long window, final int focused) { - final boolean focus = (focused == 1); + final boolean focus = (focused == GL_TRUE); if (wasActive != focus) { if (!wasActive) { @@ -200,7 +200,6 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { wasActive = !wasActive; } - } }); @@ -219,7 +218,6 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { glfwSwapInterval(0); } - // Make the window visible if (Type.Display.equals(type)) { glfwShowWindow(window); @@ -356,9 +354,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { } } - if (glfwGetWindowAttrib(window, GLFW_FOCUSED) == GL_TRUE) { - glfwPollEvents(); - } + glfwPollEvents(); // Subclasses just call GLObjectManager clean up objects here // it is safe .. for now. @@ -381,7 +377,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { public void run() { if (listener == null) { throw new IllegalStateException("SystemListener is not set on context!" - + "Must set with JmeContext.setSystemListner()."); + + "Must set with JmeContext.setSystemListener()."); } registerNatives(); From e9e4b4a1224e18aefee8793070d9872e8e2145fe Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 3 Sep 2015 13:32:06 +0100 Subject: [PATCH 21/94] Improved audio renderer information log message to include more information and to print out in a format consistent with the GL renderer information. --- .../jme3/audio/openal/ALAudioRenderer.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index 62f04018a..0878e23fa 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -31,22 +31,20 @@ */ package com.jme3.audio.openal; -import com.jme3.audio.AudioSource.Status; import com.jme3.audio.*; +import com.jme3.audio.AudioSource.Status; import com.jme3.math.Vector3f; import com.jme3.util.BufferUtils; import com.jme3.util.NativeObjectManager; + import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import static com.jme3.audio.openal.AL.*; -import static com.jme3.audio.openal.ALC.*; -import static com.jme3.audio.openal.EFX.*; public class ALAudioRenderer implements AudioRenderer, Runnable { @@ -102,16 +100,6 @@ public class ALAudioRenderer implements AudioRenderer, Runnable { return; } - String deviceName = alc.alcGetString(ALC.ALC_DEVICE_SPECIFIER); - - logger.log(Level.INFO, "Audio Device: {0}", deviceName); - logger.log(Level.INFO, "Audio Vendor: {0}", al.alGetString(AL_VENDOR)); - logger.log(Level.INFO, "Audio Renderer: {0}", al.alGetString(AL_RENDERER)); - logger.log(Level.INFO, "Audio Version: {0}", al.alGetString(AL_VERSION)); - - logger.log(Level.INFO, "ALC extensions: {0}", alc.alcGetString(ALC.ALC_EXTENSIONS)); - logger.log(Level.INFO, "AL extensions: {0}", al.alGetString(AL_EXTENSIONS)); - // Find maximum # of sources supported by this implementation ArrayList channelList = new ArrayList(); for (int i = 0; i < MAX_NUM_CHANNELS; i++) { @@ -131,7 +119,25 @@ public class ALAudioRenderer implements AudioRenderer, Runnable { ib = BufferUtils.createIntBuffer(channels.length); chanSrcs = new AudioSource[channels.length]; - logger.log(Level.INFO, "AudioRenderer supports {0} channels", channels.length); + final String deviceName = alc.alcGetString(ALC.ALC_DEVICE_SPECIFIER); + + logger.log(Level.INFO, "Audio Renderer Information\n" + + " * Device: {0}\n" + + " * Vendor: {1}\n" + + " * Renderer: {2}\n" + + " * Version: {3}\n" + + " * Supported channels: {4}\n" + + " * ALC extensions: {5}\n" + + " * AL extensions: {6}", + new Object[]{ + deviceName, + al.alGetString(AL_VENDOR), + al.alGetString(AL_RENDERER), + al.alGetString(AL_VERSION), + channels.length, + alc.alcGetString(ALC.ALC_EXTENSIONS), + al.alGetString(AL_EXTENSIONS) + }); // Pause device is a feature used specifically on Android // where the application could be closed but still running, @@ -153,7 +159,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable { alc.alcGetInteger(EFX.ALC_MAX_AUXILIARY_SENDS, ib, 1); auxSends = ib.get(0); - logger.log(Level.INFO, "Audio max auxilary sends: {0}", auxSends); + logger.log(Level.INFO, "Audio max auxiliary sends: {0}", auxSends); // create slot ib.position(0).limit(1); From 616dadc4981d739c121285df8426fc3863f622cc Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Thu, 3 Sep 2015 09:46:34 -0400 Subject: [PATCH 22/94] Deprecate ColoredTextured.j3md --- .../main/resources/Common/MatDefs/Misc/ColoredTextured.j3md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/ColoredTextured.j3md b/jme3-core/src/main/resources/Common/MatDefs/Misc/ColoredTextured.j3md index 6b74c3074..497b7d0a7 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/ColoredTextured.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/ColoredTextured.j3md @@ -1,3 +1,4 @@ +Exception This material definition is deprecated. Please use Unshaded.j3md instead. MaterialDef Colored Textured { MaterialParameters { @@ -17,4 +18,4 @@ MaterialDef Colored Textured { Technique { } -} \ No newline at end of file +} From 139ba573bc3bd8e1bd428e8c1baf3067850bab8c Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Fri, 4 Sep 2015 10:35:14 -0400 Subject: [PATCH 23/94] travis-ci: try to fix incorrect .travis.yml file --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fb7588262..eb7e58253 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ install: script: - ./gradlew check - ./gradlew createZipDistribution - - [ $TRAVIS_BRANCH == 'master' ] && [ $TRAVIS_PULL_REQUEST == 'false' ] && ./gradlew uploadArchives || : + - "[ $TRAVIS_BRANCH == 'master' ] && [ $TRAVIS_PULL_REQUEST == 'false' ] && ./gradlew uploadArchives || :" before_deploy: - export RELEASE_DIST=$(ls build/distributions/*.zip) From cb7d13948812d51ac0fdab95cebb086ca01e87bc Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sat, 5 Sep 2015 13:55:59 -0400 Subject: [PATCH 24/94] GLSLCompat: support GLES compatibility --- .../Common/MatDefs/Misc/Unshaded.frag | 2 +- .../Common/MatDefs/Misc/Unshaded.vert | 2 +- .../Common/ShaderLib/GLSL150Compat.glsllib | 14 -------- .../Common/ShaderLib/GLSLCompat.glsllib | 34 +++++++++++++++++++ 4 files changed, 36 insertions(+), 16 deletions(-) delete mode 100644 jme3-core/src/main/resources/Common/ShaderLib/GLSL150Compat.glsllib create mode 100644 jme3-core/src/main/resources/Common/ShaderLib/GLSLCompat.glsllib diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.frag b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.frag index 73df8d740..e615d8f1e 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.frag +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.frag @@ -1,4 +1,4 @@ -#import "Common/ShaderLib/GLSL150Compat.glsllib" +#import "Common/ShaderLib/GLSLCompat.glsllib" #if defined(HAS_GLOWMAP) || defined(HAS_COLORMAP) || (defined(HAS_LIGHTMAP) && !defined(SEPARATE_TEXCOORD)) #define NEED_TEXCOORD1 diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.vert b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.vert index 454708d92..ba7899ca5 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.vert @@ -1,4 +1,4 @@ -#import "Common/ShaderLib/GLSL150Compat.glsllib" +#import "Common/ShaderLib/GLSLCompat.glsllib" #import "Common/ShaderLib/Skinning.glsllib" #import "Common/ShaderLib/Instancing.glsllib" diff --git a/jme3-core/src/main/resources/Common/ShaderLib/GLSL150Compat.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/GLSL150Compat.glsllib deleted file mode 100644 index 87f07a3f5..000000000 --- a/jme3-core/src/main/resources/Common/ShaderLib/GLSL150Compat.glsllib +++ /dev/null @@ -1,14 +0,0 @@ -#if __VERSION__ >= 130 -out vec4 outFragColor; -# define texture1D texture -# define texture2D texture -# define texture3D texture -# define texture2DLod texture -# if defined VERTEX_SHADER -# define varying out -# define attribute in -# elif defined FRAGMENT_SHADER -# define varying in -# define gl_FragColor outFragColor -# endif -#endif \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/ShaderLib/GLSLCompat.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/GLSLCompat.glsllib new file mode 100644 index 000000000..3a3173997 --- /dev/null +++ b/jme3-core/src/main/resources/Common/ShaderLib/GLSLCompat.glsllib @@ -0,0 +1,34 @@ +#if defined _GL_ES_ +# define hfloat highp float +# define hvec2 highp vec2 +# define hvec3 highp vec3 +# define hvec4 highp vec4 +# define lfloat lowp float +# define lvec2 lowp vec2 +# define lvec3 lowp vec3 +# define lvec4 lowp vec4 +#else +# define hfloat float +# define hvec2 vec2 +# define hvec3 vec3 +# define hvec4 vec4 +# define lfloat float +# define lvec2 vec2 +# define lvec3 vec3 +# define lvec4 vec4 +#endif + +#if __VERSION__ >= 130 +out vec4 outFragColor; +# define texture1D texture +# define texture2D texture +# define texture3D texture +# define texture2DLod texture +# if defined VERTEX_SHADER +# define varying out +# define attribute in +# elif defined FRAGMENT_SHADER +# define varying in +# define gl_FragColor outFragColor +# endif +#endif \ No newline at end of file From 148c78a9433aead07303d0aefe09155db907dd5d Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sat, 5 Sep 2015 14:03:32 -0400 Subject: [PATCH 25/94] GL: make lwjgl implementation classes final --- jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java | 1 + jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java | 2 +- .../src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java | 2 +- .../src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java | 2 +- .../src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java | 2 +- settings.gradle | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java index 4a9380cf4..1b9c3f94d 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java @@ -161,6 +161,7 @@ public interface GL { public static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518; public static final int GL_TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519; public static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A; + public static final int GL_TEXTURE_BASE_LEVEL = 0x813C; public static final int GL_TEXTURE_MAG_FILTER = 0x2800; public static final int GL_TEXTURE_MAX_LEVEL = 0x813D; public static final int GL_TEXTURE_MIN_FILTER = 0x2801; diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java index bf99c84eb..2b7df131a 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java @@ -13,7 +13,7 @@ import java.nio.ShortBuffer; import com.jme3.renderer.opengl.GL4; import org.lwjgl.opengl.*; -public class LwjglGL implements GL, GL2, GL3, GL4 { +public final class LwjglGL implements GL, GL2, GL3, GL4 { private static void checkLimit(Buffer buffer) { if (buffer == null) { diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java index 2c6a63fd7..e3b9e6c77 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -13,7 +13,7 @@ import org.lwjgl.opengl.GL15; import org.lwjgl.opengl.GL20; import org.lwjgl.opengl.GLSync; -public class LwjglGLExt implements GLExt { +public final class LwjglGLExt implements GLExt { private static void checkLimit(Buffer buffer) { if (buffer == null) { diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java index 159000a6c..40571f5ed 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java @@ -13,7 +13,7 @@ import org.lwjgl.opengl.EXTFramebufferObject; * * @author Kirill Vainer */ -public class LwjglGLFboEXT implements GLFbo { +public final class LwjglGLFboEXT implements GLFbo { private static void checkLimit(Buffer buffer) { if (buffer == null) { diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java index acc540273..14e0cc9e6 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java @@ -11,7 +11,7 @@ import org.lwjgl.opengl.GL30; * * @author Kirill Vainer */ -public class LwjglGLFboGL3 implements GLFbo { +public final class LwjglGLFboGL3 implements GLFbo { private static void checkLimit(Buffer buffer) { if (buffer == null) { diff --git a/settings.gradle b/settings.gradle index 89b8fc075..6aad1b374 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,7 +36,7 @@ include 'jme3-testdata' include 'jme3-examples' if(buildAndroidExamples == "true"){ - include 'jme3-android-examples' + include 'jme3-android-examples' } if(buildSdkProject == "true"){ From e93fb65bca47a10c2e5b702465feb1c8b4ccbea3 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sat, 5 Sep 2015 14:04:01 -0400 Subject: [PATCH 26/94] sdk build: enable nbm signing (if key exists) --- sdk/nbproject/platform.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/nbproject/platform.properties b/sdk/nbproject/platform.properties index 73f8590d7..65b38a709 100644 --- a/sdk/nbproject/platform.properties +++ b/sdk/nbproject/platform.properties @@ -1,4 +1,6 @@ branding.token=jmonkeyplatform +keystore=../nbproject/private/keystore.jks +nbm_alias=jmeupdates cluster.path=\ ${nbplatform.active.dir}/extide:\ ${nbplatform.active.dir}/harness:\ From 4f1477735db1c274bb0a1a87ebcba14f8f43851f Mon Sep 17 00:00:00 2001 From: Dokthar Date: Sun, 6 Sep 2015 12:57:35 +0200 Subject: [PATCH 27/94] SDK scenecomposer :reduce the arrows line width from 3 to 2. --- .../src/com/jme3/gde/scenecomposer/SceneEditTool.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneEditTool.java b/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneEditTool.java index 9c7ad75cb..c2a4753c4 100644 --- a/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneEditTool.java +++ b/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/SceneEditTool.java @@ -453,9 +453,9 @@ public abstract class SceneEditTool { Geometry arrowX = new Geometry("arrowX", new Arrow(new Vector3f(arrowSize, 0, 0))); Geometry arrowY = new Geometry("arrowY", new Arrow(new Vector3f(0, arrowSize, 0))); Geometry arrowZ = new Geometry("arrowZ", new Arrow(new Vector3f(0, 0, arrowSize))); - arrowX.getMesh().setLineWidth(3f); - arrowY.getMesh().setLineWidth(3f); - arrowZ.getMesh().setLineWidth(3f); + arrowX.getMesh().setLineWidth(2f); + arrowY.getMesh().setLineWidth(2f); + arrowZ.getMesh().setLineWidth(2f); axis.attachChild(arrowX); axis.attachChild(arrowY); axis.attachChild(arrowZ); From 7e0bd4a385f9c9c975096167271c7b6d10daf3a4 Mon Sep 17 00:00:00 2001 From: Dokthar Date: Sun, 6 Sep 2015 13:07:55 +0200 Subject: [PATCH 28/94] SDK scenecomposer : fix issue #332 cursor doesn't move the position of the cursor wasn't updated, now it's work fine --- .../gde/scenecomposer/tools/SelectTool.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/tools/SelectTool.java b/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/tools/SelectTool.java index 87f3c283f..013cfb492 100644 --- a/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/tools/SelectTool.java +++ b/sdk/jme3-scenecomposer/src/com/jme3/gde/scenecomposer/tools/SelectTool.java @@ -10,6 +10,7 @@ import com.jme3.gde.core.sceneexplorer.nodes.JmeSpatial; import com.jme3.gde.core.sceneviewer.SceneViewerTopComponent; import com.jme3.gde.scenecomposer.SceneEditTool; import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.terrain.Terrain; @@ -33,7 +34,7 @@ import org.openide.loaders.DataObject; */ public class SelectTool extends SceneEditTool { - private boolean wasDraggingR = false; + private boolean wasDraggingR, wasDraggingL = false; private boolean wasDownR = false; /** @@ -52,13 +53,24 @@ public class SelectTool extends SceneEditTool { */ @Override public void actionPrimary(Vector2f screenCoord, boolean pressed, final JmeNode rootNode, DataObject dataObject) { - + if (!pressed){ + if (!wasDraggingL) { + Vector3f result = pickWorldLocation(getCamera(), screenCoord, rootNode); + if (result != null) { + if (toolController.isSnapToGrid()) { + result.set(Math.round(result.x), result.y, Math.round(result.z)); + } + toolController.doSetCursorLocation(result); + } + } + wasDraggingL = false; + } } @Override public void actionSecondary(final Vector2f screenCoord, boolean pressed, final JmeNode rootNode, DataObject dataObject) { if (pressed) { - Spatial selected = toolController.getSelectedSpatial(); + Spatial selected;// = toolController.getSelectedSpatial(); // mouse down if (!wasDraggingR && !wasDownR) { // wasn't dragging and was not down already @@ -137,6 +149,7 @@ public class SelectTool extends SceneEditTool { @Override public void draggedPrimary(Vector2f screenCoord, boolean pressed, JmeNode rootNode, DataObject currentDataObject) { + wasDraggingL = pressed; } @Override From e552fb11012946963d77f44b4326dcdb1c41ee8e Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sun, 6 Sep 2015 17:07:16 -0400 Subject: [PATCH 29/94] travis-ci: don't try to decrypt updates key for PRs --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index eb7e58253..48e75d0b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,8 +43,7 @@ deploy: before_install: - git fetch --unshallow - - openssl aes-256-cbc -K $encrypted_a1949b55824a_key -iv $encrypted_a1949b55824a_iv - -in private/www-updater.key.enc -out private/www-updater.key -d + - "[ $TRAVIS_PULL_REQUEST == 'false' ] && openssl aes-256-cbc -K $encrypted_a1949b55824a_key -iv $encrypted_a1949b55824a_iv -in private/www-updater.key.enc -out private/www-updater.key -d || :" # before_install: # required libs for android build tools From 71e2a7efef679ef6cb01baca45422b24f405dcad Mon Sep 17 00:00:00 2001 From: Dokthar Date: Wed, 9 Sep 2015 20:27:57 +0200 Subject: [PATCH 30/94] SDK Terrain Editor : changed the paintTool tooltip text to match the tool actions --- .../src/com/jme3/gde/terraineditor/Bundle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/Bundle.properties b/sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/Bundle.properties index 1f7cfa66f..69d691908 100644 --- a/sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/Bundle.properties +++ b/sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/Bundle.properties @@ -124,7 +124,7 @@ TerrainEditorTopComponent.AbsoluteCheckbox.tooltip=Define the height to adjust t TerrainEditorTopComponent.slopeLockCheckbox.tooltip=Contains the slope between the two slope nodes TerrainEditorTopComponent.borderDistanceLabel.tooltip=Distance means how far from the terrain's edge the border will be raised (thickness of the border). TerrainEditorTopComponent.borderHeightLAbel.tooltip=Height means how high the border will go (also accept negative values). -TerrainEditorTopComponent.paintButton.toolTipText=Erase a texture from the terrain +TerrainEditorTopComponent.paintButton.toolTipText=Paint a texture on the terrain. RMB to Erase. TerrainEditorTopComponent.paintButton.text= RenameTerrainVisualPanel1.ranemeLabel.text=Rename Terrain Alphamaps RenameTerrainVisualPanel1.renameField.text= From edd183a2be86af90f5bea680fc2b953d8bf0177f Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Wed, 9 Sep 2015 22:43:37 -0400 Subject: [PATCH 31/94] GLRenderer: fix texture update regression introduced in 9f3a145dd7bd083c21b302e0faaf46eddfd82237 --- .../com/jme3/renderer/opengl/GLRenderer.java | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index dd30da089..15307a9b2 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -1950,6 +1950,28 @@ public class GLRenderer implements Renderer { } } + /** + * Ensures that the texture is bound to the given unit + * and that the unit is currently active (for modification). + * + * @param target The texture target, one of GL_TEXTURE_*** + * @param img The image texture to bind + * @param unit At what unit to bind the texture. + */ + private void bindTextureAndUnit(int target, Image img, int unit) { + if (context.boundTextureUnit != unit) { + gl.glActiveTexture(GL.GL_TEXTURE0 + unit); + context.boundTextureUnit = unit; + } + if (context.boundTextures[unit] != img) { + gl.glBindTexture(target, img.getId()); + context.boundTextures[unit] = img; + statistics.onTextureUse(img, true); + } else { + statistics.onTextureUse(img, false); + } + } + /** * Uploads the given image to the GL driver. * @@ -1973,17 +1995,7 @@ public class GLRenderer implements Renderer { // bind texture int target = convertTextureType(type, img.getMultiSamples(), -1); - if (context.boundTextures[unit] != img) { - if (context.boundTextureUnit != unit) { - gl.glActiveTexture(GL.GL_TEXTURE0 + unit); - context.boundTextureUnit = unit; - } - - gl.glBindTexture(target, texId); - context.boundTextures[unit] = img; - - statistics.onTextureUse(img, true); - } + bindTextureAndUnit(target, img, unit); if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired()) { // Image does not have mipmaps, but they are required. @@ -2092,6 +2104,7 @@ public class GLRenderer implements Renderer { img.clearUpdateNeeded(); } + @Override public void setTexture(int unit, Texture tex) { Image image = tex.getImage(); if (image.isUpdateNeeded() || (image.isGeneratedMipmapsRequired() && !image.isMipmapsGenerated())) { @@ -2118,23 +2131,8 @@ public class GLRenderer implements Renderer { int texId = image.getId(); assert texId != -1; - Image[] textures = context.boundTextures; - - int type = convertTextureType(tex.getType(), image.getMultiSamples(), -1); - if (textures[unit] != image) { - if (context.boundTextureUnit != unit) { - gl.glActiveTexture(GL.GL_TEXTURE0 + unit); - context.boundTextureUnit = unit; - } - - gl.glBindTexture(type, texId); - textures[unit] = image; - - statistics.onTextureUse(image, true); - } else { - statistics.onTextureUse(image, false); - } - + int target = convertTextureType(tex.getType(), image.getMultiSamples(), -1); + bindTextureAndUnit(target, image, unit); setupTextureParams(tex); } From 584567140982b8b70a09ed1d35dfc439b84fb9c2 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Thu, 10 Sep 2015 22:33:08 -0400 Subject: [PATCH 32/94] GLRenderer: enable seamless cubemap globally --- .../java/com/jme3/renderer/RenderContext.java | 3 --- .../com/jme3/renderer/opengl/GLRenderer.java | 17 +++++------------ 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java index 16c07dc71..4b51e0727 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java @@ -255,8 +255,6 @@ public class RenderContext { public ColorRGBA clearColor = new ColorRGBA(0,0,0,0); - public boolean seamlessCubemap = false; - /** * Reset the RenderContext to default GL state */ @@ -308,6 +306,5 @@ public class RenderContext { depthFunc = RenderState.TestFunction.LessOrEqual; alphaFunc = RenderState.TestFunction.Greater; clearColor.set(0,0,0,0); - seamlessCubemap = false; } } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 15307a9b2..de11d7898 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -503,6 +503,11 @@ public class GLRenderer implements Renderer { // Initialize default state.. gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1); + + if (caps.contains(Caps.SeamlessCubemap)) { + // Enable this globally. Should be OK. + gl.glEnable(GLExt.GL_TEXTURE_CUBE_MAP_SEAMLESS); + } if (caps.contains(Caps.CoreProfile)) { // Core Profile requires VAO to be bound. @@ -1833,18 +1838,6 @@ public class GLRenderer implements Renderer { gl.glTexParameteri(target, GL.GL_TEXTURE_MIN_FILTER, minFilter); image.getLastTextureState().minFilter = tex.getMinFilter(); } - if (caps.contains(Caps.SeamlessCubemap) && tex.getType() == Texture.Type.CubeMap) { - if (haveMips && !context.seamlessCubemap) { - // We can enable seamless cubemap filtering. - gl.glEnable(GLExt.GL_TEXTURE_CUBE_MAP_SEAMLESS); - context.seamlessCubemap = true; - } else if (!haveMips && context.seamlessCubemap) { - // For skyboxes (no mipmaps), disable seamless cubemap filtering. - gl.glDisable(GLExt.GL_TEXTURE_CUBE_MAP_SEAMLESS); - context.seamlessCubemap = false; - } - } - if (tex.getAnisotropicFilter() > 1) { if (caps.contains(Caps.TextureFilterAnisotropic)) { gl.glTexParameterf(target, From f80364a8c2979f810e9d03e65ba0ae942a69e52e Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Thu, 10 Sep 2015 22:34:12 -0400 Subject: [PATCH 33/94] GLRenderer: don't set depth function twice --- .../src/main/java/com/jme3/renderer/RenderContext.java | 6 +++--- .../src/main/java/com/jme3/renderer/opengl/GLRenderer.java | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java index 4b51e0727..8287a270e 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java @@ -241,12 +241,12 @@ public class RenderContext { public IDList attribIndexList = new IDList(); /** - * depth tets function + * depth test function */ - public RenderState.TestFunction depthFunc = RenderState.TestFunction.LessOrEqual; + public RenderState.TestFunction depthFunc = RenderState.TestFunction.Less; /** - * alpha tets function + * alpha test function */ public RenderState.TestFunction alphaFunc = RenderState.TestFunction.Greater; diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index de11d7898..2d8287817 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -614,17 +614,16 @@ public class GLRenderer implements Renderer { if (state.isDepthTest() && !context.depthTestEnabled) { gl.glEnable(GL.GL_DEPTH_TEST); - gl.glDepthFunc(convertTestFunction(context.depthFunc)); context.depthTestEnabled = true; } else if (!state.isDepthTest() && context.depthTestEnabled) { gl.glDisable(GL.GL_DEPTH_TEST); context.depthTestEnabled = false; } - if (state.getDepthFunc() != context.depthFunc) { + if (state.isDepthTest() && state.getDepthFunc() != context.depthFunc) { gl.glDepthFunc(convertTestFunction(state.getDepthFunc())); context.depthFunc = state.getDepthFunc(); } - + if (state.isDepthWrite() && !context.depthWriteEnabled) { gl.glDepthMask(true); context.depthWriteEnabled = true; From 8fdc0f9c904c28717a7bfe4529f82bc40df79489 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Thu, 10 Sep 2015 23:08:50 -0400 Subject: [PATCH 34/94] GLRenderer: avoid useless glActiveTexture calls --- .../com/jme3/renderer/opengl/GLRenderer.java | 59 ++++++++++++++----- .../jme3/texture/image/LastTextureState.java | 2 + 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 2d8287817..17e9c1055 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -1432,7 +1432,7 @@ public class GLRenderer implements Renderer { // NOTE: For depth textures, sets nearest/no-mips mode // Required to fix "framebuffer unsupported" // for old NVIDIA drivers! - setupTextureParams(tex); + setupTextureParams(0, tex); } glfbo.glFramebufferTexture2DEXT(GLFbo.GL_FRAMEBUFFER_EXT, @@ -1816,7 +1816,7 @@ public class GLRenderer implements Renderer { } @SuppressWarnings("fallthrough") - private void setupTextureParams(Texture tex) { + private void setupTextureParams(int unit, Texture tex) { Image image = tex.getImage(); int target = convertTextureType(tex.getType(), image != null ? image.getMultiSamples() : 1, -1); @@ -1829,20 +1829,23 @@ public class GLRenderer implements Renderer { // filter things if (image.getLastTextureState().magFilter != tex.getMagFilter()) { int magFilter = convertMagFilter(tex.getMagFilter()); + bindTextureAndUnit(target, image, unit); gl.glTexParameteri(target, GL.GL_TEXTURE_MAG_FILTER, magFilter); image.getLastTextureState().magFilter = tex.getMagFilter(); } if (image.getLastTextureState().minFilter != tex.getMinFilter()) { int minFilter = convertMinFilter(tex.getMinFilter(), haveMips); + bindTextureAndUnit(target, image, unit); gl.glTexParameteri(target, GL.GL_TEXTURE_MIN_FILTER, minFilter); image.getLastTextureState().minFilter = tex.getMinFilter(); } - if (tex.getAnisotropicFilter() > 1) { - if (caps.contains(Caps.TextureFilterAnisotropic)) { - gl.glTexParameterf(target, - GLExt.GL_TEXTURE_MAX_ANISOTROPY_EXT, - tex.getAnisotropicFilter()); - } + if (caps.contains(Caps.TextureFilterAnisotropic) + && image.getLastTextureState().anisoFilter != tex.getAnisotropicFilter()) { + bindTextureAndUnit(target, image, unit); + gl.glTexParameterf(target, + GLExt.GL_TEXTURE_MAX_ANISOTROPY_EXT, + tex.getAnisotropicFilter()); + image.getLastTextureState().anisoFilter = tex.getAnisotropicFilter(); } // repeat modes @@ -1850,6 +1853,7 @@ public class GLRenderer implements Renderer { case ThreeDimensional: case CubeMap: // cubemaps use 3D coords if (gl2 != null && image.getLastTextureState().rWrap != tex.getWrap(WrapAxis.R)) { + bindTextureAndUnit(target, image, unit); gl2.glTexParameteri(target, GL2.GL_TEXTURE_WRAP_R, convertWrapMode(tex.getWrap(WrapAxis.R))); image.getLastTextureState().rWrap = tex.getWrap(WrapAxis.R); } @@ -1857,10 +1861,12 @@ public class GLRenderer implements Renderer { case TwoDimensional: case TwoDimensionalArray: if (image.getLastTextureState().tWrap != tex.getWrap(WrapAxis.T)) { + bindTextureAndUnit(target, image, unit); gl.glTexParameteri(target, GL.GL_TEXTURE_WRAP_T, convertWrapMode(tex.getWrap(WrapAxis.T))); image.getLastTextureState().tWrap = tex.getWrap(WrapAxis.T); } if (image.getLastTextureState().sWrap != tex.getWrap(WrapAxis.S)) { + bindTextureAndUnit(target, image, unit); gl.glTexParameteri(target, GL.GL_TEXTURE_WRAP_S, convertWrapMode(tex.getWrap(WrapAxis.S))); image.getLastTextureState().sWrap = tex.getWrap(WrapAxis.S); } @@ -1869,9 +1875,10 @@ public class GLRenderer implements Renderer { throw new UnsupportedOperationException("Unknown texture type: " + tex.getType()); } - if(tex.isNeedCompareModeUpdate() && gl2 != null){ + if (tex.isNeedCompareModeUpdate() && gl2 != null) { // R to Texture compare mode if (tex.getShadowCompareMode() != Texture.ShadowCompareMode.Off) { + bindTextureAndUnit(target, image, unit); gl2.glTexParameteri(target, GL2.GL_TEXTURE_COMPARE_MODE, GL2.GL_COMPARE_R_TO_TEXTURE); gl2.glTexParameteri(target, GL2.GL_DEPTH_TEXTURE_MODE, GL2.GL_INTENSITY); if (tex.getShadowCompareMode() == Texture.ShadowCompareMode.GreaterOrEqual) { @@ -1879,12 +1886,16 @@ public class GLRenderer implements Renderer { } else { gl2.glTexParameteri(target, GL2.GL_TEXTURE_COMPARE_FUNC, GL.GL_LEQUAL); } - }else{ + } else { + bindTextureAndUnit(target, image, unit); //restoring default value gl2.glTexParameteri(target, GL2.GL_TEXTURE_COMPARE_MODE, GL.GL_NONE); } tex.compareModeUpdated(); } + + // If at this point we didn't bind the texture, bind it now + bindTextureOnly(target, image, unit); } /** @@ -1964,6 +1975,28 @@ public class GLRenderer implements Renderer { } } + /** + * Ensures that the texture is bound to the given unit, + * but does not care if the unit is active (for rendering). + * + * @param target The texture target, one of GL_TEXTURE_*** + * @param img The image texture to bind + * @param unit At what unit to bind the texture. + */ + private void bindTextureOnly(int target, Image img, int unit) { + if (context.boundTextures[unit] != img) { + if (context.boundTextureUnit != unit) { + gl.glActiveTexture(GL.GL_TEXTURE0 + unit); + context.boundTextureUnit = unit; + } + gl.glBindTexture(target, img.getId()); + context.boundTextures[unit] = img; + statistics.onTextureUse(img, true); + } else { + statistics.onTextureUse(img, false); + } + } + /** * Uploads the given image to the GL driver. * @@ -1985,7 +2018,7 @@ public class GLRenderer implements Renderer { statistics.onNewTexture(); } - // bind texture + // bind texture int target = convertTextureType(type, img.getMultiSamples(), -1); bindTextureAndUnit(target, img, unit); @@ -2123,9 +2156,7 @@ public class GLRenderer implements Renderer { int texId = image.getId(); assert texId != -1; - int target = convertTextureType(tex.getType(), image.getMultiSamples(), -1); - bindTextureAndUnit(target, image, unit); - setupTextureParams(tex); + setupTextureParams(unit, tex); } public void modifyTexture(Texture tex, Image pixels, int x, int y) { diff --git a/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java b/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java index e7f2a2a14..3b85563ef 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java @@ -45,6 +45,7 @@ public final class LastTextureState { public Texture.WrapMode sWrap, tWrap, rWrap; public Texture.MagFilter magFilter; public Texture.MinFilter minFilter; + public int anisoFilter = 0; public LastTextureState() { // All parameters initialized to null (meaning unset). @@ -56,5 +57,6 @@ public final class LastTextureState { rWrap = null; magFilter = null; minFilter = null; + anisoFilter = 0; } } From 9da4b78830f168c93862e65c117667b96b5e43b9 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Thu, 10 Sep 2015 23:09:15 -0400 Subject: [PATCH 35/94] GLRenderer: disable unused vertex attributes before rendering instead of after --- .../src/main/java/com/jme3/renderer/opengl/GLRenderer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 17e9c1055..0aa0d693f 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -2650,12 +2650,13 @@ public class GLRenderer implements Renderer { } } + clearVertexAttribs(); + if (indices != null) { drawTriangleList(indices, mesh, count); } else { drawTriangleArray(mesh.getMode(), count, mesh.getVertexCount()); } - clearVertexAttribs(); } public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { From e9245a753b8336f6ff3a9deac76b048e2362ed7f Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Thu, 10 Sep 2015 23:10:13 -0400 Subject: [PATCH 36/94] GLTracer: generate syntax highlighting and easier to read output --- .../com/jme3/renderer/opengl/GLTracer.java | 358 ++++++++++++++---- 1 file changed, 281 insertions(+), 77 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java index 1b0d70749..a2775ba0a 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java @@ -36,8 +36,14 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.nio.Buffer; import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.DoubleBuffer; +import java.nio.FloatBuffer; import java.nio.IntBuffer; +import java.nio.LongBuffer; +import java.nio.ShortBuffer; import java.util.HashMap; /** @@ -51,6 +57,17 @@ public final class GLTracer implements InvocationHandler { private final IntMap constMap; private static final HashMap> nonEnumArgMap = new HashMap>(); + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_BRIGHT = "\u001B[1m"; + private static final String ANSI_BLACK = "\u001B[30m"; + private static final String ANSI_RED = "\u001B[31m"; + private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_YELLOW = "\u001B[33m"; + private static final String ANSI_BLUE = "\u001B[34m"; + private static final String ANSI_MAGENTA = "\u001B[35m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_WHITE = "\u001B[37m"; + private static void noEnumArgs(String method, int... argSlots) { IntMap argSlotsMap = new IntMap(); for (int argSlot : argSlots) { @@ -174,100 +191,287 @@ public final class GLTracer implements InvocationHandler { new GLTracer(glInterface, constMap)); } - private String translateInteger(String method, int value, int argIndex) { - IntMap argSlotMap = nonEnumArgMap.get(method); - if (argSlotMap != null && argSlotMap.containsKey(argIndex)) { - return Integer.toString(value); - } + private void printStyle(String style, String string) { + System.out.print(style + string + ANSI_RESET); + } + + private void print(String string) { + System.out.print(string); + } + + private void printInt(int value) { + print(Integer.toString(value)); + } + + private void printEnum(int value) { String enumName = constMap.get(value); if (enumName != null) { - return enumName; + if (enumName.startsWith("GL_")) { + enumName = enumName.substring(3); + } + if (enumName.endsWith("_EXT") || enumName.endsWith("_ARB")) { + enumName = enumName.substring(0, enumName.length() - 4); + } + printStyle(ANSI_GREEN, enumName); + } else { + printStyle(ANSI_GREEN, "ENUM_" + Integer.toHexString(value)); + } + } + + private void printIntOrEnum(String method, int value, int argIndex) { + IntMap argSlotMap = nonEnumArgMap.get(method); + if (argSlotMap != null && argSlotMap.containsKey(argIndex)) { + printInt(value); } else { - return "GL_ENUM_" + Integer.toHexString(value); - //throw new IllegalStateException("Untranslatable enum encountered on " + method + - // " at argument " + argIndex + " with value " + value); + printEnum(value); } } - private String translateString(String value) { - return "\"" + value.replaceAll("\0", "\\\\0") + "\""; + private void printNewLine() { + System.out.println(); } - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - Object result = method.invoke(obj, args); - String methodName = method.getName(); + private void printString(String value) { + if (value.length() > 150) { + value = value.substring(0, 150) + "..."; + } + StringBuilder sb = new StringBuilder(); + sb.append(ANSI_YELLOW); + sb.append("\""); + sb.append(ANSI_RESET); + for (String line : value.split("\n")) { + sb.append(ANSI_YELLOW); + sb.append(line.replaceAll("\0", "\\\\0")); + sb.append(ANSI_RESET); + sb.append("\n"); + } + if (sb.length() > 1 && sb.charAt(sb.length() - 1) == '\n') { + sb.setLength(sb.length() - 1); + } + sb.append(ANSI_YELLOW); + sb.append("\""); + sb.append(ANSI_RESET); + print(sb.toString()); + } + + private void printBoolean(boolean bool) { + printStyle(ANSI_BLUE, bool ? "true" : "false"); + } + + private void printBuffer(Buffer buffer) { + StringBuilder sb = new StringBuilder(); + sb.append(ANSI_MAGENTA); + if (buffer instanceof ByteBuffer) { + sb.append("byte"); + } else if (buffer instanceof ShortBuffer) { + sb.append("short"); + } else if (buffer instanceof CharBuffer) { + sb.append("char"); + } else if (buffer instanceof FloatBuffer) { + sb.append("float"); + } else if (buffer instanceof IntBuffer) { + sb.append("int"); + } else if (buffer instanceof LongBuffer) { + sb.append("long"); + } else if (buffer instanceof DoubleBuffer) { + sb.append("double"); + } else { + throw new UnsupportedOperationException(); + } + sb.append(ANSI_RESET); + sb.append("["); + + if (buffer.position() == 0 + && buffer.limit() == buffer.capacity()) { + // Common case. Just print buffer size. + sb.append(buffer.capacity()); + } else { + sb.append("pos=").append(buffer.position()); + sb.append(" lim=").append(buffer.limit()); + sb.append(" cap=").append(buffer.capacity()); + } + + sb.append("]"); + print(sb.toString()); + } + + private void printMethodName(String methodName) { if (methodName.startsWith("gl")) { - System.out.print(methodName); - System.out.print("("); - if (args != null) { - Class[] paramTypes = method.getParameterTypes(); - for (int i = 0; i < args.length; i++) { - if (paramTypes[i] == int.class) { - int val = (Integer)args[i]; - System.out.print(translateInteger(methodName, val, i)); - } else if (paramTypes[i] == String.class) { - System.out.print(translateString((String)args[i])); - } else if (paramTypes[i] == String[].class) { - String[] arr = (String[]) args[i]; - if (arr.length == 1) { - if (arr[0].length() > 150) { - System.out.print("\"" + arr[0].substring(0, 150) + "...\""); - } else { - System.out.print("\"" + arr[0] + "\""); - } - } else { - System.out.print("String[" + arr.length + "]"); - } - } else if (args[i] instanceof IntBuffer) { - IntBuffer buf = (IntBuffer) args[i]; - if (buf.capacity() == 16) { - int val = buf.get(0); - System.out.print("out=" + translateInteger(methodName, val, i)); - } else if (buf.capacity() == 1) { - System.out.print("out=" + buf.get(0)); - } else { - System.out.print(args[i]); - } - } else if (args[i] instanceof ByteBuffer) { - ByteBuffer bb = (ByteBuffer)args[i]; - if (bb.capacity() == 250) { - if (bb.get(0) != 0) { - System.out.print("out=GL_TRUE"); - } else { - System.out.print("out=GL_FALSE"); - } - } else { - System.out.print(args[i]); - } - } else { - System.out.print(args[i]); - } - - if (i != args.length - 1) { - System.out.print(", "); - } + // GL calls which actually draw (as opposed to change state) + // will be printed in darker color + methodName = methodName.substring(2); + if (methodName.equals("Clear") + || methodName.equals("DrawRangeElements")) { + print(methodName); + } else { + if (methodName.endsWith("EXT")) { + methodName = methodName.substring(0, methodName.length() - 3); } + printStyle(ANSI_BRIGHT, methodName); } + } else if (methodName.equals("resetStats")) { + printStyle(ANSI_RED, "-- frame boundary --"); + } + } + + private void printArgsClear(int mask) { + boolean needAPipe = false; + print("("); + if ((mask & GL.GL_COLOR_BUFFER_BIT) != 0) { + printStyle(ANSI_GREEN, "COLOR_BUFFER_BIT"); + needAPipe = true; + } + if ((mask & GL.GL_DEPTH_BUFFER_BIT) != 0) { + if (needAPipe) { + print(" | "); + } + printStyle(ANSI_GREEN, "DEPTH_BUFFER_BIT"); + } + if ((mask & GL.GL_STENCIL_BUFFER_BIT) != 0) { + if (needAPipe) { + print(" | "); + } + printStyle(ANSI_GREEN, "STENCIL_BUFFER_BIT"); + } + print(")"); + } + + private void printArgsTexParameter(Object[] args) { + print("("); - System.out.print(")"); + int target = (Integer) args[0]; + int param = (Integer) args[1]; + int value = (Integer) args[2]; - if (method.getReturnType() != void.class) { - if (result instanceof String) { - System.out.println(" = " + translateString((String)result)); - } else if (method.getReturnType() == int.class) { - int val = (Integer)result; - System.out.println(" = " + translateInteger(methodName, val, -1)); - } else if (method.getReturnType() == boolean.class) { - boolean val = (Boolean)result; - if (val) System.out.println(" = GL_TRUE"); - else System.out.println(" = GL_FALSE"); + printEnum(target); + print(", "); + printEnum(param); + print(", "); + + if (param == GL.GL_TEXTURE_BASE_LEVEL + || param == GL.GL_TEXTURE_MAX_LEVEL) { + printInt(value); + } else { + printEnum(value); + } + + print(")"); + } + + private void printOut() { + printStyle(ANSI_CYAN, "out="); + } + + private void printResult(String methodName, Object result, Class returnType) { + if (returnType != void.class) { + print(" = "); + if (result instanceof String) { + printString((String) result); + } else if (returnType == int.class) { + int val = (Integer) result; + printIntOrEnum(methodName, val, -1); + } else if (returnType == boolean.class) { + printBoolean((Boolean)result); + } else { + print(" = ???"); + } + } + } + + private void printNull() { + printStyle(ANSI_BLUE, "null"); + } + + private void printArgs(String methodName, Object[] args, Class[] paramTypes) { + if (methodName.equals("glClear")) { + printArgsClear((Integer)args[0]); + return; + } else if (methodName.equals("glTexParameteri")) { + printArgsTexParameter(args); + return; + } + + if (args == null) { + print("()"); + return; + } + + print("("); + for (int i = 0; i < args.length; i++) { + if (paramTypes[i] == int.class) { + int val = (Integer)args[i]; + printIntOrEnum(methodName, val, i); + } else if (paramTypes[i] == boolean.class) { + printBoolean((Boolean)args[i]); + } else if (paramTypes[i] == String.class) { + printString((String)args[i]); + } else if (paramTypes[i] == String[].class) { + String[] arr = (String[]) args[i]; + if (arr.length == 1) { + printString(arr[0]); } else { - System.out.println(" = ???"); + print("string[" + arr.length + "]"); } + } else if (args[i] instanceof IntBuffer) { + IntBuffer buf = (IntBuffer) args[i]; + if (buf.capacity() == 16) { + int val = buf.get(0); + printOut(); + printIntOrEnum(methodName, val, i); + } else if (buf.capacity() == 1) { + printOut(); + print(Integer.toString(buf.get(0))); + } else { + printBuffer(buf); + } + } else if (args[i] instanceof ByteBuffer) { + ByteBuffer bb = (ByteBuffer)args[i]; + if (bb.capacity() == 250) { + printOut(); + printBoolean(bb.get(0) != 0); + } else { + printBuffer(bb); + } + } else if (args[i] instanceof Buffer) { + printBuffer((Buffer)args[i]); + } else if (args[i] != null) { + print(args[i].toString()); } else { - System.out.println(); + printNull(); + } + + if (i != args.length - 1) { + System.out.print(", "); + } + } + print(")"); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + printMethodName(methodName); + + if (methodName.startsWith("gl")) { + try { + // Try to evaluate result first, so we can see output values. + Object result = method.invoke(obj, args); + printArgs(methodName, args, method.getParameterTypes()); + printResult(methodName, result, method.getReturnType()); + printNewLine(); + return result; + } catch (Throwable ex) { + // Execution failed, print args anyway + // but output values will be incorrect. + printArgs(methodName, args, method.getParameterTypes()); + printNewLine(); + System.out.println("\tException occurred!"); + System.out.println(ex.toString()); + throw ex; } + } else { + printNewLine(); + return method.invoke(obj, args); } - return result; } } From 86439c2c2b307c2ae54c0ebe1898f3b096989ec2 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Fri, 11 Sep 2015 13:51:48 -0400 Subject: [PATCH 37/94] native bullet: fix JNI crash in ray / sweep test Method return type does not match call function return type --- jme3-bullet-native/src/native/cpp/jmeBulletUtil.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jme3-bullet-native/src/native/cpp/jmeBulletUtil.cpp b/jme3-bullet-native/src/native/cpp/jmeBulletUtil.cpp index 04b56a545..a51bcf833 100644 --- a/jme3-bullet-native/src/native/cpp/jmeBulletUtil.cpp +++ b/jme3-bullet-native/src/native/cpp/jmeBulletUtil.cpp @@ -351,7 +351,7 @@ void jmeBulletUtil::addResult(JNIEnv* env, jobject resultlist, btVector3* hitnor env->SetFloatField(singleresult, jmeClasses::PhysicsRay_hitfraction, m_hitFraction); env->SetObjectField(singleresult, jmeClasses::PhysicsRay_collisionObject, up1->javaCollisionObject); - env->CallVoidMethod(resultlist, jmeClasses::PhysicsRay_addmethod, singleresult); + env->CallBooleanMethod(resultlist, jmeClasses::PhysicsRay_addmethod, singleresult); if (env->ExceptionCheck()) { env->Throw(env->ExceptionOccurred()); return; @@ -371,7 +371,7 @@ void jmeBulletUtil::addSweepResult(JNIEnv* env, jobject resultlist, btVector3* h env->SetFloatField(singleresult, jmeClasses::PhysicsSweep_hitfraction, m_hitFraction); env->SetObjectField(singleresult, jmeClasses::PhysicsSweep_collisionObject, up1->javaCollisionObject); - env->CallVoidMethod(resultlist, jmeClasses::PhysicsSweep_addmethod, singleresult); + env->CallBooleanMethod(resultlist, jmeClasses::PhysicsSweep_addmethod, singleresult); if (env->ExceptionCheck()) { env->Throw(env->ExceptionOccurred()); return; From 62eede87b3e69e90851ab8c28a1ae6d6177514e6 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sat, 12 Sep 2015 17:54:43 -0400 Subject: [PATCH 38/94] niftygui build: add niftygui repository reference to pom --- jme3-niftygui/build.gradle | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/jme3-niftygui/build.gradle b/jme3-niftygui/build.gradle index abe25f6e4..fe4f46462 100644 --- a/jme3-niftygui/build.gradle +++ b/jme3-niftygui/build.gradle @@ -14,3 +14,16 @@ dependencies { compile 'lessvoid:nifty-default-controls:1.4.1' compile 'lessvoid:nifty-style-black:1.4.1' } + +uploadArchives { + repositories.mavenDeployer { + pom.project { + repositories { + repository { + id "nifty-maven-repo.sourceforge.net" + url "http://nifty-gui.sourceforge.net/nifty-maven-repo" + } + } + } + } +} \ No newline at end of file From 88bf9d458014427cb15ceb80cbd75850845ab11b Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Sun, 13 Sep 2015 20:23:07 +0100 Subject: [PATCH 39/94] Reverted some changes to light classes which was not meant to be committed as part of #314. --- .../src/main/java/com/jme3/light/Light.java | 16 ----------- .../main/java/com/jme3/light/PointLight.java | 28 +++++++++---------- 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/light/Light.java b/jme3-core/src/main/java/com/jme3/light/Light.java index f050729c2..b1c48be7a 100644 --- a/jme3-core/src/main/java/com/jme3/light/Light.java +++ b/jme3-core/src/main/java/com/jme3/light/Light.java @@ -122,22 +122,6 @@ public abstract class Light implements Savable, Cloneable { setColor(color); } - /** - * Default constructor for Light. - */ - public Light() { - - } - - /** - * Constructor which allows setting of the color. - * - * @param color the color to apply to this light. - */ - public Light(final ColorRGBA color) { - this.color = color; - } - /** * Returns the color of the light. * diff --git a/jme3-core/src/main/java/com/jme3/light/PointLight.java b/jme3-core/src/main/java/com/jme3/light/PointLight.java index 2505a2a50..ff3b3295f 100644 --- a/jme3-core/src/main/java/com/jme3/light/PointLight.java +++ b/jme3-core/src/main/java/com/jme3/light/PointLight.java @@ -53,7 +53,7 @@ import java.io.IOException; * In addition to a position, point lights also have a radius which * can be used to attenuate the influence of the light depending on the * distance between the light and the effected object. - * + * */ public class PointLight extends Light { @@ -84,7 +84,7 @@ public class PointLight extends Light { super(color); setPosition(position); } - + /** * Creates a PointLight at the given position, with the given color and the * given radius @@ -96,7 +96,7 @@ public class PointLight extends Light { this(position, color); setRadius(radius); } - + /** * Creates a PointLight at the given position, with the given radius * @param position the position in world space @@ -119,10 +119,10 @@ public class PointLight extends Light { /** * Returns the world space position of the light. - * + * * @return the world space position of the light. - * - * @see PointLight#setPosition(com.jme3.math.Vector3f) + * + * @see PointLight#setPosition(com.jme3.math.Vector3f) */ public Vector3f getPosition() { return position; @@ -130,7 +130,7 @@ public class PointLight extends Light { /** * Set the world space position of the light. - * + * * @param position the world space position of the light. */ public final void setPosition(Vector3f position) { @@ -140,7 +140,7 @@ public class PointLight extends Light { /** * Returns the radius of the light influence. A radius of 0 means * the light has no attenuation. - * + * * @return the radius of the light */ public float getRadius() { @@ -155,9 +155,9 @@ public class PointLight extends Light { * is greater than the light's radius, then the pixel will not be * effected by this light, if the distance is less than the radius, then * the magnitude of the influence is equal to distance / radius. - * + * * @param radius the radius of the light influence. - * + * * @throws IllegalArgumentException If radius is negative */ public final void setRadius(float radius) { @@ -192,11 +192,11 @@ public class PointLight extends Light { } else { // Sphere v. box collision return FastMath.abs(box.getCenter().x - position.x) < radius + box.getXExtent() - && FastMath.abs(box.getCenter().y - position.y) < radius + box.getYExtent() - && FastMath.abs(box.getCenter().z - position.z) < radius + box.getZExtent(); + && FastMath.abs(box.getCenter().y - position.y) < radius + box.getYExtent() + && FastMath.abs(box.getCenter().z - position.z) < radius + box.getZExtent(); } } - + @Override public boolean intersectsFrustum(Camera camera, TempVars vars) { if (this.radius == 0) { @@ -210,7 +210,7 @@ public class PointLight extends Light { return true; } } - + @Override public void write(JmeExporter ex) throws IOException { super.write(ex); From b68035570d4f04cc9b070ef45376310a5ee29ddc Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Sun, 13 Sep 2015 20:26:18 +0100 Subject: [PATCH 40/94] Removed a TODO note and added key count implementation (rough for now). --- .../src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java | 2 -- .../src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java index f17c08534..b1063d6b9 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwJoystickInput.java @@ -64,8 +64,6 @@ public class GlfwJoystickInput implements JoyInput { @Override public Joystick[] loadJoysticks(final InputManager inputManager) { - // TODO: Implement - for (int i = 0; i < GLFW_JOYSTICK_LAST; i++) { if (glfwJoystickPresent(i) == GL11.GL_TRUE) { final String name = glfwGetJoystickName(i); diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java index 4768290d7..5b7d8e267 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java @@ -80,7 +80,8 @@ public class GlfwKeyInput implements KeyInput { } public int getKeyCount() { - return 0; // TODO: How do we figure this out? + // This might not be correct + return GLFW_KEY_LAST - GLFW_KEY_SPACE; } public void update() { From dd6356eff1d9ea747c9f6c4d1628950d7c39e710 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Sun, 13 Sep 2015 20:30:36 +0100 Subject: [PATCH 41/94] Reverted some more light code which should not be in this branch. --- .../src/main/java/com/jme3/light/AmbientLight.java | 6 +++--- .../main/java/com/jme3/light/DirectionalLight.java | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/light/AmbientLight.java b/jme3-core/src/main/java/com/jme3/light/AmbientLight.java index 101a4efc7..8dd5f9266 100644 --- a/jme3-core/src/main/java/com/jme3/light/AmbientLight.java +++ b/jme3-core/src/main/java/com/jme3/light/AmbientLight.java @@ -45,7 +45,7 @@ import com.jme3.util.TempVars; * regardless of the model's location. The material's ambient color is * multiplied by the ambient light color to get the final ambient color of * an object. - * + * * @author Kirill Vainer */ public class AmbientLight extends Light { @@ -61,12 +61,12 @@ public class AmbientLight extends Light { public boolean intersectsBox(BoundingBox box, TempVars vars) { return true; } - + @Override public boolean intersectsFrustum(Camera camera, TempVars vars) { return true; } - + @Override public void computeLastDistance(Spatial owner) { } diff --git a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java index 333154b2d..87fbf695a 100644 --- a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java +++ b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java @@ -85,10 +85,10 @@ public class DirectionalLight extends Light { /** * Returns the direction vector of the light. - * + * * @return The direction vector of the light. - * - * @see DirectionalLight#setDirection(com.jme3.math.Vector3f) + * + * @see DirectionalLight#setDirection(com.jme3.math.Vector3f) */ public Vector3f getDirection() { return direction; @@ -99,7 +99,7 @@ public class DirectionalLight extends Light { *

* Represents the direction the light is shining. * (1, 0, 0) would represent light shining in the +X direction. - * + * * @param dir the direction of the light. */ public final void setDirection(Vector3f dir){ @@ -113,12 +113,12 @@ public class DirectionalLight extends Light { public boolean intersectsBox(BoundingBox box, TempVars vars) { return true; } - + @Override public boolean intersectsFrustum(Camera camera, TempVars vars) { return true; } - + @Override public Type getType() { return Type.Directional; From d8e964b2f0f7b1c7e729fcd748cbc9c6ea062596 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Sun, 13 Sep 2015 20:43:41 +0100 Subject: [PATCH 42/94] Added copyright headers to some files. --- .../java/com/jme3/audio/lwjgl/LwjglALC.java | 31 ++++++++++++++++++ .../java/com/jme3/audio/lwjgl/LwjglEFX.java | 31 ++++++++++++++++++ .../java/com/jme3/renderer/lwjgl/LwjglGL.java | 31 ++++++++++++++++++ .../com/jme3/renderer/lwjgl/LwjglGLExt.java | 31 ++++++++++++++++++ .../jme3/renderer/lwjgl/LwjglGLFboEXT.java | 31 ++++++++++++++++++ .../jme3/renderer/lwjgl/LwjglGLFboGL3.java | 31 ++++++++++++++++++ .../com/jme3/system/lwjgl/LwjglDisplay.java | 32 ++++++++++++++++++- .../system/lwjgl/LwjglOffscreenBuffer.java | 32 ++++++++++++++++++- .../com/jme3/system/lwjgl/LwjglWindow.java | 3 +- 9 files changed, 249 insertions(+), 4 deletions(-) diff --git a/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java index e0aed80ee..b0b1bc1e3 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java @@ -1,3 +1,34 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package com.jme3.audio.lwjgl; import com.jme3.audio.openal.ALC; diff --git a/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java index 2ddd09ed8..ecd67211f 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglEFX.java @@ -1,3 +1,34 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package com.jme3.audio.lwjgl; import com.jme3.audio.openal.EFX; diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java index 80e7f3513..187b232f2 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java @@ -1,3 +1,34 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package com.jme3.renderer.lwjgl; import com.jme3.renderer.RendererException; diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java index 00b3f688c..2ed2ec215 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -1,3 +1,34 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package com.jme3.renderer.lwjgl; import com.jme3.renderer.RendererException; diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java index 969d7ae1d..5d9fe8dc1 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java @@ -1,3 +1,34 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package com.jme3.renderer.lwjgl; import com.jme3.renderer.RendererException; diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java index aa15aeb09..5a7a9825e 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java @@ -1,3 +1,34 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package com.jme3.renderer.lwjgl; import com.jme3.renderer.RendererException; diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java index 4a13c30af..3eb749adc 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglDisplay.java @@ -1,8 +1,38 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package com.jme3.system.lwjgl; /** * @author Daniel Johansson - * @since 2015-08-11 */ public class LwjglDisplay extends LwjglWindow { diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java index e374e1841..8fdde51af 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglOffscreenBuffer.java @@ -1,10 +1,40 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package com.jme3.system.lwjgl; import com.jme3.system.JmeContext; /** * @author Daniel Johansson - * @since 2015-08-11 */ public class LwjglOffscreenBuffer extends LwjglWindow { diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java index 6eac34cdc..c5cb35e03 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java @@ -60,8 +60,7 @@ import static org.lwjgl.system.MemoryUtil.NULL; /** * A wrapper class over the GLFW framework in LWJGL 3. * - * @author Daniel Johansson (dannyjo) - * @since 3.1 + * @author Daniel Johansson */ public abstract class LwjglWindow extends LwjglContext implements Runnable { From 45f8893f13ea19c16287da95734baabb10e3ff5d Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sun, 13 Sep 2015 18:40:08 -0400 Subject: [PATCH 43/94] bounding: properly implement collideWith against other bounds --- .../main/java/com/jme3/bounding/BoundingBox.java | 10 ++++++++++ .../main/java/com/jme3/bounding/BoundingSphere.java | 12 +++++++++++- .../main/java/com/jme3/bounding/BoundingVolume.java | 13 +++++++------ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java index bdba19fe3..11ee8b2ea 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java +++ b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java @@ -790,6 +790,7 @@ public class BoundingBox extends BoundingVolume { } } + @Override public int collideWith(Collidable other, CollisionResults results) { if (other instanceof Ray) { Ray ray = (Ray) other; @@ -802,6 +803,13 @@ public class BoundingBox extends BoundingVolume { return 1; } return 0; + } else if (other instanceof BoundingVolume) { + if (intersects((BoundingVolume) other)) { + CollisionResult r = new CollisionResult(); + results.addCollision(r); + return 1; + } + return 0; } else { throw new UnsupportedCollisionException("With: " + other.getClass().getSimpleName()); } @@ -818,6 +826,8 @@ public class BoundingBox extends BoundingVolume { return 1; } return 0; + } else if (other instanceof BoundingVolume) { + return intersects((BoundingVolume) other) ? 1 : 0; } else { throw new UnsupportedCollisionException("With: " + other.getClass().getSimpleName()); } diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java index d5dd56203..adb763f83 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java +++ b/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java @@ -1013,17 +1013,27 @@ public class BoundingSphere extends BoundingVolume { } else if (other instanceof Triangle){ Triangle t = (Triangle) other; return collideWithTri(t, results); + } else if (other instanceof BoundingVolume) { + if (intersects((BoundingVolume)other)) { + CollisionResult result = new CollisionResult(); + results.addCollision(result); + return 1; + } + return 0; } else { throw new UnsupportedCollisionException(); } } - @Override public int collideWith(Collidable other) { + @Override + public int collideWith(Collidable other) { if (other instanceof Ray) { Ray ray = (Ray) other; return collideWithRay(ray); } else if (other instanceof Triangle){ return super.collideWith(other); + } else if (other instanceof BoundingVolume) { + return intersects((BoundingVolume)other) ? 1 : 0; } else { throw new UnsupportedCollisionException(); } diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingVolume.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingVolume.java index f9be2e573..4491fcedf 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/BoundingVolume.java +++ b/jme3-core/src/main/java/com/jme3/bounding/BoundingVolume.java @@ -327,12 +327,13 @@ public abstract class BoundingVolume implements Savable, Cloneable, Collidable { public int collideWith(Collidable other) { TempVars tempVars = TempVars.get(); - CollisionResults tempResults = tempVars.collisionResults; - tempResults.clear(); - int retval = collideWith(other, tempResults); - tempVars.release(); - return retval; + try { + CollisionResults tempResults = tempVars.collisionResults; + tempResults.clear(); + return collideWith(other, tempResults); + } finally { + tempVars.release(); + } } - } From 1fa6c4ac11619aeeeaf9d617749865e5d93c8120 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sun, 13 Sep 2015 22:05:18 -0400 Subject: [PATCH 44/94] bounding: move intersection algorithms to shared class These algorithms are to be shared with the light filter. --- .../java/com/jme3/bounding/BoundingBox.java | 13 +---- .../com/jme3/bounding/BoundingSphere.java | 23 +-------- .../java/com/jme3/bounding/Intersection.java | 48 ++++++++++++++++++- 3 files changed, 50 insertions(+), 34 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java index 11ee8b2ea..06f78154e 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java +++ b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java @@ -593,18 +593,7 @@ public class BoundingBox extends BoundingVolume { * @see BoundingVolume#intersectsSphere(com.jme3.bounding.BoundingSphere) */ public boolean intersectsSphere(BoundingSphere bs) { - assert Vector3f.isValidVector(center) && Vector3f.isValidVector(bs.center); - - if (FastMath.abs(center.x - bs.center.x) < bs.getRadius() - + xExtent - && FastMath.abs(center.y - bs.center.y) < bs.getRadius() - + yExtent - && FastMath.abs(center.z - bs.center.z) < bs.getRadius() - + zExtent) { - return true; - } - - return false; + return bs.intersectsBoundingBox(this); } /** diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java index adb763f83..5eecf040a 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java +++ b/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java @@ -670,15 +670,7 @@ public class BoundingSphere extends BoundingVolume { * @see com.jme.bounding.BoundingVolume#intersectsSphere(com.jme.bounding.BoundingSphere) */ public boolean intersectsSphere(BoundingSphere bs) { - assert Vector3f.isValidVector(center) && Vector3f.isValidVector(bs.center); - - TempVars vars = TempVars.get(); - - Vector3f diff = center.subtract(bs.center, vars.vect1); - float rsum = getRadius() + bs.getRadius(); - boolean eq = (diff.dot(diff) <= rsum * rsum); - vars.release(); - return eq; + return Intersection.intersect(bs, center, radius); } /* @@ -687,18 +679,7 @@ public class BoundingSphere extends BoundingVolume { * @see com.jme.bounding.BoundingVolume#intersectsBoundingBox(com.jme.bounding.BoundingBox) */ public boolean intersectsBoundingBox(BoundingBox bb) { - assert Vector3f.isValidVector(center) && Vector3f.isValidVector(bb.center); - - if (FastMath.abs(bb.center.x - center.x) < getRadius() - + bb.xExtent - && FastMath.abs(bb.center.y - center.y) < getRadius() - + bb.yExtent - && FastMath.abs(bb.center.z - center.z) < getRadius() - + bb.zExtent) { - return true; - } - - return false; + return Intersection.intersect(bb, center, radius); } /* diff --git a/jme3-core/src/main/java/com/jme3/bounding/Intersection.java b/jme3-core/src/main/java/com/jme3/bounding/Intersection.java index 9deb62abc..c627e23e5 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/Intersection.java +++ b/jme3-core/src/main/java/com/jme3/bounding/Intersection.java @@ -41,10 +41,56 @@ import static java.lang.Math.min; /** * This class includes some utility methods for computing intersection * between bounding volumes and triangles. + * * @author Kirill */ -public class Intersection { +public final class Intersection { + private Intersection() { + } + + public static boolean intersect(BoundingSphere sphere, Vector3f center, float radius) { + assert Vector3f.isValidVector(center) && Vector3f.isValidVector(sphere.center); + + TempVars vars = TempVars.get(); + try { + Vector3f diff = center.subtract(sphere.center, vars.vect1); + float rsum = sphere.getRadius() + radius; + return (diff.dot(diff) <= rsum * rsum); + } finally { + vars.release(); + } + } + + public static boolean intersect(BoundingBox bbox, Vector3f center, float radius) { + assert Vector3f.isValidVector(center) && Vector3f.isValidVector(bbox.center); + + // Arvo's algorithm + float distSqr = radius * radius; + + float minX = bbox.center.x - bbox.xExtent; + float maxX = bbox.center.x + bbox.xExtent; + + float minY = bbox.center.y - bbox.yExtent; + float maxY = bbox.center.y + bbox.yExtent; + + float minZ = bbox.center.z - bbox.zExtent; + float maxZ = bbox.center.z + bbox.zExtent; + + if (center.x < minX) distSqr -= FastMath.sqr(center.x - minX); + else if (center.x > maxX) distSqr -= FastMath.sqr(center.x - maxX); + + + if (center.y < minY) distSqr -= FastMath.sqr(center.y - minY); + else if (center.y > maxY) distSqr -= FastMath.sqr(center.y - maxY); + + + if (center.z < minZ) distSqr -= FastMath.sqr(center.z - minZ); + else if (center.z > maxZ) distSqr -= FastMath.sqr(center.z - maxZ); + + return distSqr > 0; + } + private static final void findMinMax(float x0, float x1, float x2, Vector3f minMax) { minMax.set(x0, x0, 0); if (x1 < minMax.x) { From 31383778d9d578c74d12e6cf2aaaddb582f8c082 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sun, 13 Sep 2015 22:06:32 -0400 Subject: [PATCH 45/94] light: ensure directional lights are sorted to be always first --- jme3-core/src/main/java/com/jme3/light/AmbientLight.java | 2 ++ jme3-core/src/main/java/com/jme3/light/DirectionalLight.java | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/light/AmbientLight.java b/jme3-core/src/main/java/com/jme3/light/AmbientLight.java index 8dd5f9266..58676851c 100644 --- a/jme3-core/src/main/java/com/jme3/light/AmbientLight.java +++ b/jme3-core/src/main/java/com/jme3/light/AmbientLight.java @@ -69,6 +69,8 @@ public class AmbientLight extends Light { @Override public void computeLastDistance(Spatial owner) { + // ambient lights must always be before directional lights. + lastDistance = -2; } @Override diff --git a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java index 87fbf695a..b8e1a1979 100644 --- a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java +++ b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java @@ -80,7 +80,9 @@ public class DirectionalLight extends Light { @Override public void computeLastDistance(Spatial owner) { - lastDistance = 0; // directional lights are always closest to their owner + // directional lights are after ambient lights + // but before all other lights. + lastDistance = -1; } /** From 6238088688be5913fd40518f2d01dbdfcaa65720 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sun, 13 Sep 2015 22:07:34 -0400 Subject: [PATCH 46/94] light: minor non-functional changes --- jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java | 2 ++ jme3-core/src/main/java/com/jme3/light/Light.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java b/jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java index 81287279e..c8456529d 100644 --- a/jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java +++ b/jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java @@ -44,6 +44,7 @@ public final class DefaultLightFilter implements LightFilter { private Camera camera; private final HashSet processedLights = new HashSet(); + @Override public void setCamera(Camera camera) { this.camera = camera; for (Light light : processedLights) { @@ -51,6 +52,7 @@ public final class DefaultLightFilter implements LightFilter { } } + @Override public void filterLights(Geometry geometry, LightList filteredLightList) { TempVars vars = TempVars.get(); try { diff --git a/jme3-core/src/main/java/com/jme3/light/Light.java b/jme3-core/src/main/java/com/jme3/light/Light.java index b1c48be7a..bac775aeb 100644 --- a/jme3-core/src/main/java/com/jme3/light/Light.java +++ b/jme3-core/src/main/java/com/jme3/light/Light.java @@ -197,7 +197,7 @@ public abstract class Light implements Savable, Cloneable { public abstract boolean intersectsBox(BoundingBox box, TempVars vars); /** - * Determines if the lgiht intersects with the given camera frustum. + * Determines if the light intersects with the given camera frustum. * * For non-local lights, such as {@link DirectionalLight directional lights}, * {@link AmbientLight ambient lights}, or {@link PointLight point lights} From 22dde7f718ed9da787f94d6cfc46557bb6d1615b Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sun, 13 Sep 2015 22:08:17 -0400 Subject: [PATCH 47/94] point light: more accurate sphere vs box filter --- jme3-core/src/main/java/com/jme3/light/PointLight.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/light/PointLight.java b/jme3-core/src/main/java/com/jme3/light/PointLight.java index ff3b3295f..1f515e71c 100644 --- a/jme3-core/src/main/java/com/jme3/light/PointLight.java +++ b/jme3-core/src/main/java/com/jme3/light/PointLight.java @@ -33,12 +33,12 @@ package com.jme3.light; import com.jme3.bounding.BoundingBox; import com.jme3.bounding.BoundingVolume; +import com.jme3.bounding.Intersection; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; import com.jme3.math.ColorRGBA; -import com.jme3.math.FastMath; import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; import com.jme3.scene.Spatial; @@ -191,9 +191,7 @@ public class PointLight extends Light { return true; } else { // Sphere v. box collision - return FastMath.abs(box.getCenter().x - position.x) < radius + box.getXExtent() - && FastMath.abs(box.getCenter().y - position.y) < radius + box.getYExtent() - && FastMath.abs(box.getCenter().z - position.z) < radius + box.getZExtent(); + return Intersection.intersect(box, position, radius); } } From c41058a5a04e17e509cab24282597ed3f2490722 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sun, 13 Sep 2015 22:09:20 -0400 Subject: [PATCH 48/94] spot light: fix broken filter for infinite range --- jme3-core/src/main/java/com/jme3/light/SpotLight.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/light/SpotLight.java b/jme3-core/src/main/java/com/jme3/light/SpotLight.java index 9b551d3d8..de24280ab 100644 --- a/jme3-core/src/main/java/com/jme3/light/SpotLight.java +++ b/jme3-core/src/main/java/com/jme3/light/SpotLight.java @@ -227,7 +227,10 @@ public class SpotLight extends Light { @Override public boolean intersectsFrustum(Camera cam, TempVars vars) { - + if (spotRange == 0) { + // The algorithm below does not support infinite spot range. + return true; + } Vector3f farPoint = vars.vect1.set(position).addLocal(vars.vect2.set(direction).multLocal(spotRange)); for (int i = 5; i >= 0; i--) { //check origin against the plane From 81b5c48fb014eb127eef356e5eb40b242898a34d Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sun, 13 Sep 2015 22:11:11 -0400 Subject: [PATCH 49/94] unit test: add unit tests for bounds and light filter / sort --- .../jme3/collision/BoundingCollisionTest.java | 166 ++++++++++++++++ .../com/jme3/collision/CollisionUtil.java | 80 ++++++++ .../java/com/jme3/light/LightFilterTest.java | 179 ++++++++++++++++++ .../java/com/jme3/light/LightSortTest.java | 115 +++++++++++ 4 files changed, 540 insertions(+) create mode 100644 jme3-core/src/test/java/com/jme3/collision/BoundingCollisionTest.java create mode 100644 jme3-core/src/test/java/com/jme3/collision/CollisionUtil.java create mode 100644 jme3-core/src/test/java/com/jme3/light/LightFilterTest.java create mode 100644 jme3-core/src/test/java/com/jme3/light/LightSortTest.java diff --git a/jme3-core/src/test/java/com/jme3/collision/BoundingCollisionTest.java b/jme3-core/src/test/java/com/jme3/collision/BoundingCollisionTest.java new file mode 100644 index 000000000..7c3104292 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/collision/BoundingCollisionTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2009-2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.collision; + +import static com.jme3.collision.CollisionUtil.*; + +import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; +import com.jme3.math.FastMath; +import com.jme3.math.Ray; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.Quad; +import org.junit.Test; + +/** + * Tests collision detection between bounding volumes. + * + * @author Kirill Vainer + */ +public class BoundingCollisionTest { + + @Test + public void testBoxBoxCollision() { + BoundingBox box1 = new BoundingBox(Vector3f.ZERO, 1, 1, 1); + BoundingBox box2 = new BoundingBox(Vector3f.ZERO, 1, 1, 1); + checkCollision(box1, box2, 1); + + // Put it at the very edge - should still intersect. + box2.setCenter(new Vector3f(2f, 0f, 0f)); + checkCollision(box1, box2, 1); + + // Put it a wee bit farther - no intersection expected + box2.setCenter(new Vector3f(2f + FastMath.ZERO_TOLERANCE, 0, 0)); + checkCollision(box1, box2, 0); + + // Check the corners. + box2.setCenter(new Vector3f(2f, 2f, 2f)); + checkCollision(box1, box2, 1); + + box2.setCenter(new Vector3f(2f, 2f, 2f + FastMath.ZERO_TOLERANCE)); + checkCollision(box1, box2, 0); + } + + @Test + public void testSphereSphereCollision() { + BoundingSphere sphere1 = new BoundingSphere(1, Vector3f.ZERO); + BoundingSphere sphere2 = new BoundingSphere(1, Vector3f.ZERO); + checkCollision(sphere1, sphere2, 1); + + // Put it at the very edge - should still intersect. + sphere2.setCenter(new Vector3f(2f, 0f, 0f)); + checkCollision(sphere1, sphere2, 1); + + // Put it a wee bit farther - no intersection expected + sphere2.setCenter(new Vector3f(2f + FastMath.ZERO_TOLERANCE, 0, 0)); + checkCollision(sphere1, sphere2, 0); + } + + @Test + public void testBoxSphereCollision() { + BoundingBox box1 = new BoundingBox(Vector3f.ZERO, 1, 1, 1); + BoundingSphere sphere2 = new BoundingSphere(1, Vector3f.ZERO); + checkCollision(box1, sphere2, 1); + + // Put it at the very edge - for sphere vs. box, it will not intersect + sphere2.setCenter(new Vector3f(2f, 0f, 0f)); + checkCollision(box1, sphere2, 0); + + // Put it a wee bit closer - should intersect. + sphere2.setCenter(new Vector3f(2f - FastMath.ZERO_TOLERANCE, 0, 0)); + checkCollision(box1, sphere2, 1); + + // Test if the algorithm converts the sphere + // to a box before testing the collision (incorrect) + float sqrt3 = FastMath.sqrt(3); + + sphere2.setCenter(Vector3f.UNIT_XYZ.mult(2)); + sphere2.setRadius(sqrt3); + checkCollision(box1, sphere2, 0); + + // Make it a wee bit larger. + sphere2.setRadius(sqrt3 + FastMath.ZERO_TOLERANCE); + checkCollision(box1, sphere2, 1); + } + + @Test + public void testBoxRayCollision() { + BoundingBox box = new BoundingBox(Vector3f.ZERO, 1, 1, 1); + Ray ray = new Ray(Vector3f.ZERO, Vector3f.UNIT_Z); + + // XXX: seems incorrect, ray inside box should only generate + // one result... + checkCollision(box, ray, 2); + + ray.setOrigin(new Vector3f(0, 0, -5)); + checkCollision(box, ray, 2); + + // XXX: is this right? the ray origin is on the box's side.. + ray.setOrigin(new Vector3f(0, 0, 2f)); + checkCollision(box, ray, 0); + + ray.setOrigin(new Vector3f(0, 0, -2f)); + checkCollision(box, ray, 2); + + // parallel to the edge, touching the side + ray.setOrigin(new Vector3f(0, 1f, -2f)); + checkCollision(box, ray, 2); + + // still parallel, but not touching the side + ray.setOrigin(new Vector3f(0, 1f + FastMath.ZERO_TOLERANCE, -2f)); + checkCollision(box, ray, 0); + } + + @Test + public void testBoxTriangleCollision() { + BoundingBox box = new BoundingBox(Vector3f.ZERO, 1, 1, 1); + Geometry geom = new Geometry("geom", new Quad(1, 1)); + checkCollision(box, geom, 2); // Both triangles intersect + + // The box touches the edges of the triangles. + box.setCenter(new Vector3f(-1f, 0, 0)); + checkCollision(box, geom, 2); + + // Move it slightly farther.. + box.setCenter(new Vector3f(-1f - FastMath.ZERO_TOLERANCE, 0, 0)); + checkCollision(box, geom, 0); + + // Parallel triangle / box side, touching + box.setCenter(new Vector3f(0, 0, -1f)); + checkCollision(box, geom, 2); + + // Not touching + box.setCenter(new Vector3f(0, 0, -1f - FastMath.ZERO_TOLERANCE)); + checkCollision(box, geom, 0); + } +} diff --git a/jme3-core/src/test/java/com/jme3/collision/CollisionUtil.java b/jme3-core/src/test/java/com/jme3/collision/CollisionUtil.java new file mode 100644 index 000000000..43d7b20f0 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/collision/CollisionUtil.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2009-2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.collision; + +import com.jme3.bounding.BoundingVolume; + +/** + * Utilities for testing collision. + * + * @author Kirill Vainer + */ +final class CollisionUtil { + + private static void checkCollisionBase(Collidable a, Collidable b, int expected) { + // Test bounding volume methods + if (a instanceof BoundingVolume && b instanceof BoundingVolume) { + BoundingVolume bv1 = (BoundingVolume) a; + BoundingVolume bv2 = (BoundingVolume) b; + assert bv1.intersects(bv2) == (expected != 0); + } + + // Test standard collideWith method + CollisionResults results = new CollisionResults(); + int numCollisions = a.collideWith(b, results); + assert results.size() == numCollisions; + assert numCollisions == expected; + + // force the results to be sorted here.. + results.getClosestCollision(); + + if (results.size() > 0) { + assert results.getCollision(0) == results.getClosestCollision(); + } + if (results.size() == 1) { + assert results.getClosestCollision() == results.getFarthestCollision(); + } + } + + /** + * Tests various collisions between the two collidables and + * the transitive property. + * + * @param a First collidable + * @param b Second collidable + * @param expect Number of expected results + */ + public static void checkCollision(Collidable a, Collidable b, int expected) { + checkCollisionBase(a, b, expected); + checkCollisionBase(b, a, expected); + } +} diff --git a/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java b/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java new file mode 100644 index 000000000..0d1e71988 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2009-2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.light; + +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.Box; +import com.jme3.util.TempVars; +import org.junit.Before; +import org.junit.Test; + +/** + * Test light filtering for various light types. + * + * @author Kirill Vainer + */ +public class LightFilterTest { + + private DefaultLightFilter filter; + private Camera cam; + private Geometry geom; + private LightList list; + + private void checkFilteredLights(int expected) { + geom.updateGeometricState(); + filter.setCamera(cam); // setCamera resets the intersection cache + list.clear(); + filter.filterLights(geom, list); + assert list.size() == expected; + } + + @Before + public void setUp() { + filter = new DefaultLightFilter(); + + cam = new Camera(512, 512); + cam.setFrustumPerspective(45, 1, 1, 1000); + cam.setLocation(Vector3f.ZERO); + cam.lookAtDirection(Vector3f.UNIT_Z, Vector3f.UNIT_Y); + filter.setCamera(cam); + + Box box = new Box(1, 1, 1); + geom = new Geometry("geom", box); + geom.setLocalTranslation(0, 0, 10); + geom.updateGeometricState(); + list = new LightList(geom); + } + + @Test + public void testAmbientFiltering() { + geom.addLight(new AmbientLight()); + checkFilteredLights(1); // Ambient lights must never be filtered + } + + @Test + public void testDirectionalFiltering() { + geom.addLight(new DirectionalLight(Vector3f.UNIT_Y)); + checkFilteredLights(1); // Directional lights must never be filtered + } + + @Test + public void testPointFiltering() { + PointLight pl = new PointLight(Vector3f.ZERO); + geom.addLight(pl); + checkFilteredLights(1); // Infinite point lights must never be filtered + + // Light at origin does not intersect geom which is at Z=10 + pl.setRadius(1); + checkFilteredLights(0); + + // Put it closer to geom, the very edge of the sphere touches the box. + // Still not considered an intersection though. + pl.setPosition(new Vector3f(0, 0, 8f)); + checkFilteredLights(0); + + // And more close - now its an intersection. + pl.setPosition(new Vector3f(0, 0, 8f + FastMath.ZERO_TOLERANCE)); + checkFilteredLights(1); + + // Move the geometry away + geom.move(0, 0, FastMath.ZERO_TOLERANCE); + checkFilteredLights(0); + + // Test if the algorithm converts the sphere + // to a box before testing the collision (incorrect) + float sqrt3 = FastMath.sqrt(3); + + pl.setPosition(new Vector3f(2, 2, 8)); + pl.setRadius(sqrt3); + checkFilteredLights(0); + + // Make it a wee bit larger. + pl.setRadius(sqrt3 + FastMath.ZERO_TOLERANCE); + checkFilteredLights(1); + + // Rotate the camera so it is up, light is outside frustum. + cam.lookAtDirection(Vector3f.UNIT_Y, Vector3f.UNIT_Y); + checkFilteredLights(0); + } + + @Test + public void testSpotFiltering() { + SpotLight sl = new SpotLight(Vector3f.ZERO, Vector3f.UNIT_Z); + sl.setSpotRange(0); + geom.addLight(sl); + checkFilteredLights(1); // Infinite spot lights are only filtered + // if the geometry is outside the infinite cone. + + TempVars vars = TempVars.get(); + try { + // The spot is not touching the near plane of the camera yet, + // should still be culled. + sl.setSpotRange(1f - FastMath.ZERO_TOLERANCE); + assert !sl.intersectsFrustum(cam, vars); + // should be culled from the geometry's PoV + checkFilteredLights(0); + + // Now it touches the near plane. + sl.setSpotRange(1f); + // still culled from the geometry's PoV + checkFilteredLights(0); + assert sl.intersectsFrustum(cam, vars); + } finally { + vars.release(); + } + + // make it barely reach the geometry + sl.setSpotRange(9f); + checkFilteredLights(0); + + // make it reach the geometry (touching its bound) + sl.setSpotRange(9f + FastMath.ZERO_TOLERANCE); + checkFilteredLights(1); + + // rotate the cone a bit so it no longer faces the geom + sl.setDirection(new Vector3f(0.316f, 0, 0.948f).normalizeLocal()); + checkFilteredLights(0); + + // extent the range much farther + sl.setSpotRange(20); + checkFilteredLights(0); + + // Create box of size X=10 (double the extent) + // now, the spot will touch the box. + geom.setMesh(new Box(5, 1, 1)); + checkFilteredLights(1); + } +} diff --git a/jme3-core/src/test/java/com/jme3/light/LightSortTest.java b/jme3-core/src/test/java/com/jme3/light/LightSortTest.java new file mode 100644 index 000000000..593cc9d3e --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/light/LightSortTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2009-2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.light; + +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import org.junit.Test; + +/** + * Test light sorting (in the scene graph) for various light types. + * + * @author Kirill Vainer + */ +public class LightSortTest { + + @Test + public void testSimpleSort() { + Geometry g = new Geometry("test", new Mesh()); + LightList list = new LightList(g); + + list.add(new SpotLight(Vector3f.ZERO, Vector3f.UNIT_X)); + list.add(new PointLight(Vector3f.UNIT_X)); + list.add(new DirectionalLight(Vector3f.UNIT_X)); + list.add(new AmbientLight()); + + list.sort(true); + + assert list.get(0) instanceof AmbientLight; // Ambients always first + assert list.get(1) instanceof DirectionalLight; // .. then directionals + assert list.get(2) instanceof SpotLight; // Spot is 0 units away from geom + assert list.get(3) instanceof PointLight; // .. and point is 1 unit away. + } + + @Test + public void testSceneGraphSort() { + Node n = new Node("node"); + Geometry g = new Geometry("geom", new Mesh()); + SpotLight spot = new SpotLight(Vector3f.ZERO, Vector3f.UNIT_X); + PointLight point = new PointLight(Vector3f.UNIT_X); + DirectionalLight directional = new DirectionalLight(Vector3f.UNIT_X); + AmbientLight ambient = new AmbientLight(); + + // Some lights are on the node + n.addLight(spot); + n.addLight(point); + + // .. and some on the geometry. + g.addLight(directional); + g.addLight(ambient); + + n.attachChild(g); + n.updateGeometricState(); + + LightList list = g.getWorldLightList(); + + // check the sorting (when geom is at 0,0,0) + assert list.get(0) instanceof AmbientLight; + assert list.get(1) instanceof DirectionalLight; + assert list.get(2) instanceof SpotLight; + assert list.get(3) instanceof PointLight; + + // move the geometry closer to the point light + g.setLocalTranslation(Vector3f.UNIT_X); + n.updateGeometricState(); + + assert list.get(0) instanceof AmbientLight; + assert list.get(1) instanceof DirectionalLight; + assert list.get(2) instanceof PointLight; + assert list.get(3) instanceof SpotLight; + + // now move the point light away from the geometry + // and the spot light closer + + // XXX: doesn't work! jME can't detect that the light moved! +// point.setPosition(Vector3f.ZERO); +// spot.setPosition(Vector3f.UNIT_X); +// n.updateGeometricState(); +// +// assert list.get(0) instanceof AmbientLight; +// assert list.get(1) instanceof DirectionalLight; +// assert list.get(2) instanceof SpotLight; +// assert list.get(3) instanceof PointLight; + } +} From f32d92ef301fbfa6456695fe89f2bbbd5522eb92 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Sun, 13 Sep 2015 23:02:49 -0400 Subject: [PATCH 50/94] bounding: add bound vs. spatial, also improve unit test --- .../java/com/jme3/bounding/BoundingBox.java | 3 ++ .../com/jme3/bounding/BoundingSphere.java | 3 ++ .../jme3/collision/BoundingCollisionTest.java | 37 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java index 06f78154e..5baf3e3d6 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java +++ b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java @@ -41,6 +41,7 @@ import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; import com.jme3.math.*; import com.jme3.scene.Mesh; +import com.jme3.scene.Spatial; import com.jme3.util.TempVars; import java.io.IOException; import java.nio.FloatBuffer; @@ -799,6 +800,8 @@ public class BoundingBox extends BoundingVolume { return 1; } return 0; + } else if (other instanceof Spatial) { + return ((Spatial)other).collideWith(this, results); } else { throw new UnsupportedCollisionException("With: " + other.getClass().getSimpleName()); } diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java index 5eecf040a..3137f2cbf 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java +++ b/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java @@ -38,6 +38,7 @@ import com.jme3.collision.UnsupportedCollisionException; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.math.*; +import com.jme3.scene.Spatial; import com.jme3.util.BufferUtils; import com.jme3.util.TempVars; import java.io.IOException; @@ -1001,6 +1002,8 @@ public class BoundingSphere extends BoundingVolume { return 1; } return 0; + } else if (other instanceof Spatial) { + return ((Spatial)other).collideWith(this, results); } else { throw new UnsupportedCollisionException(); } diff --git a/jme3-core/src/test/java/com/jme3/collision/BoundingCollisionTest.java b/jme3-core/src/test/java/com/jme3/collision/BoundingCollisionTest.java index 7c3104292..506a1732d 100644 --- a/jme3-core/src/test/java/com/jme3/collision/BoundingCollisionTest.java +++ b/jme3-core/src/test/java/com/jme3/collision/BoundingCollisionTest.java @@ -162,5 +162,42 @@ public class BoundingCollisionTest { // Not touching box.setCenter(new Vector3f(0, 0, -1f - FastMath.ZERO_TOLERANCE)); checkCollision(box, geom, 0); + + // Test collisions only against one of the triangles + box.setCenter(new Vector3f(-1f, 1.5f, 0f)); + checkCollision(box, geom, 1); + + box.setCenter(new Vector3f(1.5f, -1f, 0f)); + checkCollision(box, geom, 1); + } + + @Test + public void testSphereTriangleCollision() { + BoundingSphere sphere = new BoundingSphere(1, Vector3f.ZERO); + Geometry geom = new Geometry("geom", new Quad(1, 1)); + checkCollision(sphere, geom, 2); + + // The box touches the edges of the triangles. + sphere.setCenter(new Vector3f(-1f + FastMath.ZERO_TOLERANCE, 0, 0)); + checkCollision(sphere, geom, 2); + + // Move it slightly farther.. + sphere.setCenter(new Vector3f(-1f - FastMath.ZERO_TOLERANCE, 0, 0)); + checkCollision(sphere, geom, 0); + + // Parallel triangle / box side, touching + sphere.setCenter(new Vector3f(0, 0, -1f)); + checkCollision(sphere, geom, 2); + + // Not touching + sphere.setCenter(new Vector3f(0, 0, -1f - FastMath.ZERO_TOLERANCE)); + checkCollision(sphere, geom, 0); + + // Test collisions only against one of the triangles + sphere.setCenter(new Vector3f(-0.9f, 1.2f, 0f)); + checkCollision(sphere, geom, 1); + + sphere.setCenter(new Vector3f(1.2f, -0.9f, 0f)); + checkCollision(sphere, geom, 1); } } From 78d2d6e944116fa61b24b4aee89cdf13459d2795 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Mon, 14 Sep 2015 22:51:57 -0400 Subject: [PATCH 51/94] GLRenderer: fix invalid enum error when using framebuffers --- .../com/jme3/renderer/opengl/GLRenderer.java | 190 ++++++++++-------- .../com/jme3/renderer/opengl/GLTracer.java | 18 ++ 2 files changed, 123 insertions(+), 85 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 0aa0d693f..276453117 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -1459,25 +1459,44 @@ public class GLRenderer implements Renderer { rb.getId()); } } + + private void bindFrameBuffer(FrameBuffer fb) { + if (fb == null) { + if (context.boundFBO != 0) { + glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, 0); + statistics.onFrameBufferUse(null, true); + context.boundFBO = 0; + context.boundFB = null; + } + } else { + assert fb.getId() != -1 && fb.getId() != 0; + if (context.boundFBO != fb.getId()) { + glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, fb.getId()); + context.boundFBO = fb.getId(); + context.boundFB = fb; + statistics.onFrameBufferUse(fb, true); + } else { + statistics.onFrameBufferUse(fb, false); + } + } + } public void updateFrameBuffer(FrameBuffer fb) { + if (fb.getNumColorBuffers() == 0 && fb.getDepthBuffer() == null) { + throw new IllegalArgumentException("The framebuffer: " + fb + + "\nDoesn't have any color/depth buffers"); + } + int id = fb.getId(); if (id == -1) { - // create FBO glfbo.glGenFramebuffersEXT(intBuf1); id = intBuf1.get(0); fb.setId(id); objManager.registerObject(fb); - statistics.onNewFrameBuffer(); } - if (context.boundFBO != id) { - glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, id); - // binding an FBO automatically sets draw buf to GL_COLOR_ATTACHMENT0 - context.boundDrawBuf = 0; - context.boundFBO = id; - } + bindFrameBuffer(fb); FrameBuffer.RenderBuffer depthBuf = fb.getDepthBuffer(); if (depthBuf != null) { @@ -1488,7 +1507,8 @@ public class GLRenderer implements Renderer { FrameBuffer.RenderBuffer colorBuf = fb.getColorBuffer(i); updateFrameBufferAttachment(fb, colorBuf); } - + + setReadDrawBuffers(fb); checkFrameBufferError(); fb.clearUpdateNeeded(); @@ -1516,93 +1536,45 @@ public class GLRenderer implements Renderer { } public void setMainFrameBufferOverride(FrameBuffer fb) { + mainFbOverride = null; + if (context.boundFBO == 0) { + // Main FB is now set to fb, make sure its bound + setFrameBuffer(fb); + } mainFbOverride = fb; } - public void setFrameBuffer(FrameBuffer fb) { - if (fb == null && mainFbOverride != null) { - fb = mainFbOverride; - } - - if (context.boundFB == fb) { - if (fb == null || !fb.isUpdateNeeded()) { - return; - } - } - - if (!caps.contains(Caps.FrameBuffer)) { - throw new RendererException("Framebuffer objects are not supported" - + " by the video hardware"); - } - - // generate mipmaps for last FB if needed - if (context.boundFB != null) { - for (int i = 0; i < context.boundFB.getNumColorBuffers(); i++) { - RenderBuffer rb = context.boundFB.getColorBuffer(i); - Texture tex = rb.getTexture(); - if (tex != null - && tex.getMinFilter().usesMipMapLevels()) { - setTexture(0, rb.getTexture()); - - int textureType = convertTextureType(tex.getType(), tex.getImage().getMultiSamples(), rb.getFace()); - glfbo.glGenerateMipmapEXT(textureType); - } - } + public void setReadDrawBuffers(FrameBuffer fb) { + if (gl2 == null) { + return; } - + + final int NONE = -2; + final int INITIAL = -1; + final int MRT_OFF = 100; + if (fb == null) { - // unbind any fbos - if (context.boundFBO != 0) { - glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, 0); - statistics.onFrameBufferUse(null, true); - - context.boundFBO = 0; + // Set Read/Draw buffers to initial value. + if (context.boundDrawBuf != INITIAL) { + gl2.glDrawBuffer(context.initialDrawBuf); + context.boundDrawBuf = INITIAL; } - // select back buffer - if (gl2 != null) { - if (context.boundDrawBuf != -1) { - gl2.glDrawBuffer(context.initialDrawBuf); - context.boundDrawBuf = -1; - } - if (context.boundReadBuf != -1) { - gl2.glReadBuffer(context.initialReadBuf); - context.boundReadBuf = -1; - } + if (context.boundReadBuf != INITIAL) { + gl2.glReadBuffer(context.initialReadBuf); + context.boundReadBuf = INITIAL; } - - context.boundFB = null; } else { - if (fb.getNumColorBuffers() == 0 && fb.getDepthBuffer() == null) { - throw new IllegalArgumentException("The framebuffer: " + fb - + "\nDoesn't have any color/depth buffers"); - } - - if (fb.isUpdateNeeded()) { - updateFrameBuffer(fb); - } - - // update viewport to reflect framebuffer's resolution - setViewPort(0, 0, fb.getWidth(), fb.getHeight()); - - if (context.boundFBO != fb.getId()) { - glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, fb.getId()); - statistics.onFrameBufferUse(fb, true); - - context.boundFBO = fb.getId(); - } else { - statistics.onFrameBufferUse(fb, false); - } if (fb.getNumColorBuffers() == 0) { // make sure to select NONE as draw buf - // no color buffer attached. select NONE + // no color buffer attached. if (gl2 != null) { - if (context.boundDrawBuf != -2) { + if (context.boundDrawBuf != NONE) { gl2.glDrawBuffer(GL.GL_NONE); - context.boundDrawBuf = -2; + context.boundDrawBuf = NONE; } - if (context.boundReadBuf != -2) { + if (context.boundReadBuf != NONE) { gl2.glReadBuffer(GL.GL_NONE); - context.boundReadBuf = -2; + context.boundReadBuf = NONE; } } } else { @@ -1622,7 +1594,7 @@ public class GLRenderer implements Renderer { + " by the video hardware!"); } - if (context.boundDrawBuf != 100 + fb.getNumColorBuffers()) { + if (context.boundDrawBuf != MRT_OFF + fb.getNumColorBuffers()) { intBuf16.clear(); for (int i = 0; i < fb.getNumColorBuffers(); i++) { intBuf16.put(GLFbo.GL_COLOR_ATTACHMENT0_EXT + i); @@ -1630,7 +1602,7 @@ public class GLRenderer implements Renderer { intBuf16.flip(); glext.glDrawBuffers(intBuf16); - context.boundDrawBuf = 100 + fb.getNumColorBuffers(); + context.boundDrawBuf = MRT_OFF + fb.getNumColorBuffers(); } } else { RenderBuffer rb = fb.getColorBuffer(fb.getTargetIndex()); @@ -1643,8 +1615,56 @@ public class GLRenderer implements Renderer { } } } + } + + } + + public void setFrameBuffer(FrameBuffer fb) { + if (fb == null && mainFbOverride != null) { + fb = mainFbOverride; + } + + if (context.boundFB == fb) { + if (fb == null || !fb.isUpdateNeeded()) { + return; + } + } + + if (!caps.contains(Caps.FrameBuffer)) { + throw new RendererException("Framebuffer objects are not supported" + + " by the video hardware"); + } + + // generate mipmaps for last FB if needed + if (context.boundFB != null) { + for (int i = 0; i < context.boundFB.getNumColorBuffers(); i++) { + RenderBuffer rb = context.boundFB.getColorBuffer(i); + Texture tex = rb.getTexture(); + if (tex != null + && tex.getMinFilter().usesMipMapLevels()) { + setTexture(0, rb.getTexture()); + + int textureType = convertTextureType(tex.getType(), tex.getImage().getMultiSamples(), rb.getFace()); + glfbo.glGenerateMipmapEXT(textureType); + } + } + } + + if (fb == null) { + bindFrameBuffer(null); + setReadDrawBuffers(null); + } else { + if (fb.isUpdateNeeded()) { + updateFrameBuffer(fb); + } else { + bindFrameBuffer(fb); + setReadDrawBuffers(fb); + } + + // update viewport to reflect framebuffer's resolution + setViewPort(0, 0, fb.getWidth(), fb.getHeight()); - assert fb.getId() >= 0; + assert fb.getId() > 0; assert context.boundFBO == fb.getId(); context.boundFB = fb; diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java index a2775ba0a..f252d22dc 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java @@ -336,6 +336,21 @@ public final class GLTracer implements InvocationHandler { print(")"); } + private void printArgsGetInteger(Object[] args) { + print("("); + int param = (Integer)args[0]; + IntBuffer ib = (IntBuffer) args[1]; + printEnum(param); + print(", "); + printOut(); + if (param == GL2.GL_DRAW_BUFFER || param == GL2.GL_READ_BUFFER) { + printEnum(ib.get(0)); + } else { + printInt(ib.get(0)); + } + print(")"); + } + private void printArgsTexParameter(Object[] args) { print("("); @@ -389,6 +404,9 @@ public final class GLTracer implements InvocationHandler { } else if (methodName.equals("glTexParameteri")) { printArgsTexParameter(args); return; + } else if (methodName.equals("glGetInteger")) { + printArgsGetInteger(args); + return; } if (args == null) { From 1aaf806c65a3c4e87b38e74c4d88111102ad32a1 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Mon, 14 Sep 2015 23:00:55 -0400 Subject: [PATCH 52/94] test: fix crash due to deprecated material --- .../jme3test/renderer/TestInconsistentCompareDetection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-examples/src/main/java/jme3test/renderer/TestInconsistentCompareDetection.java b/jme3-examples/src/main/java/jme3test/renderer/TestInconsistentCompareDetection.java index a93a75c16..c1f99b35d 100644 --- a/jme3-examples/src/main/java/jme3test/renderer/TestInconsistentCompareDetection.java +++ b/jme3-examples/src/main/java/jme3test/renderer/TestInconsistentCompareDetection.java @@ -63,7 +63,7 @@ public class TestInconsistentCompareDetection extends SimpleApplication { cam.setLocation(new Vector3f(-11.674385f, 7.892636f, 33.133106f)); cam.setRotation(new Quaternion(0.06426433f, 0.90940624f, -0.15329266f, 0.38125014f)); - Material m = new Material(assetManager, "Common/MatDefs/Misc/ColoredTextured.j3md"); + Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); m.setColor("Color", ColorRGBA.White); t1 = assetManager.loadTexture("Textures/Terrain/BrickWall/BrickWall.jpg"); From 6970c8db8ad25012cf165e225de1aacbb1230a31 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Mon, 14 Sep 2015 23:01:46 -0400 Subject: [PATCH 53/94] shapes: set static usage on all VBs --- jme3-core/src/main/java/com/jme3/scene/debug/Grid.java | 1 + jme3-core/src/main/java/com/jme3/scene/debug/WireFrustum.java | 2 ++ jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java | 1 + jme3-core/src/main/java/com/jme3/scene/shape/Quad.java | 1 + jme3-core/src/main/java/com/jme3/scene/shape/Sphere.java | 2 +- 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/Grid.java b/jme3-core/src/main/java/com/jme3/scene/debug/Grid.java index 5b6d0661e..563981804 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/Grid.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/Grid.java @@ -99,6 +99,7 @@ public class Grid extends Mesh { updateBound(); updateCounts(); + setStatic(); } } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/WireFrustum.java b/jme3-core/src/main/java/com/jme3/scene/debug/WireFrustum.java index c050d1edb..4f5364753 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/WireFrustum.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/WireFrustum.java @@ -35,6 +35,7 @@ import com.jme3.math.Vector3f; import com.jme3.scene.Mesh; import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer.Type; +import com.jme3.scene.VertexBuffer.Usage; import com.jme3.util.BufferUtils; import java.nio.FloatBuffer; @@ -62,6 +63,7 @@ public class WireFrustum extends Mesh { 3, 7, } ); + getBuffer(Type.Index).setUsage(Usage.Static); setMode(Mode.Lines); } diff --git a/jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java b/jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java index f0966328f..1d215eb6b 100644 --- a/jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java +++ b/jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java @@ -389,6 +389,7 @@ public class Cylinder extends Mesh { } updateBound(); + setStatic(); } @Override diff --git a/jme3-core/src/main/java/com/jme3/scene/shape/Quad.java b/jme3-core/src/main/java/com/jme3/scene/shape/Quad.java index 194535851..3e0c6ce7e 100644 --- a/jme3-core/src/main/java/com/jme3/scene/shape/Quad.java +++ b/jme3-core/src/main/java/com/jme3/scene/shape/Quad.java @@ -124,6 +124,7 @@ public class Quad extends Mesh { } updateBound(); + setStatic(); } diff --git a/jme3-core/src/main/java/com/jme3/scene/shape/Sphere.java b/jme3-core/src/main/java/com/jme3/scene/shape/Sphere.java index 89db073a7..44aa41723 100644 --- a/jme3-core/src/main/java/com/jme3/scene/shape/Sphere.java +++ b/jme3-core/src/main/java/com/jme3/scene/shape/Sphere.java @@ -299,7 +299,6 @@ public class Sphere extends Mesh { } updateBound(); - setStatic(); } /** @@ -400,6 +399,7 @@ public class Sphere extends Mesh { this.interior = interior; setGeometryData(); setIndexData(); + setStatic(); } public void read(JmeImporter e) throws IOException { From aee7d1f1956fe8638221966d29a4dad3e89c9719 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Mon, 14 Sep 2015 23:02:31 -0400 Subject: [PATCH 54/94] GLSLCompat.glsllib: fix incorrect preprocessor define --- .../src/main/resources/Common/ShaderLib/GLSLCompat.glsllib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-core/src/main/resources/Common/ShaderLib/GLSLCompat.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/GLSLCompat.glsllib index 3a3173997..3d23b7915 100644 --- a/jme3-core/src/main/resources/Common/ShaderLib/GLSLCompat.glsllib +++ b/jme3-core/src/main/resources/Common/ShaderLib/GLSLCompat.glsllib @@ -1,4 +1,4 @@ -#if defined _GL_ES_ +#if defined GL_ES # define hfloat highp float # define hvec2 highp vec2 # define hvec3 highp vec3 From 7659a7b986c1779158cb70082bf10a9a2ed1822d Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Mon, 14 Sep 2015 23:03:08 -0400 Subject: [PATCH 55/94] TestCustomAnim: fix crash when enabling HW skinning --- .../src/main/java/jme3test/model/anim/TestCustomAnim.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestCustomAnim.java b/jme3-examples/src/main/java/jme3test/model/anim/TestCustomAnim.java index e220eeb37..b5eeb5365 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestCustomAnim.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestCustomAnim.java @@ -73,6 +73,13 @@ public class TestCustomAnim extends SimpleApplication { Box box = new Box(1, 1, 1); + VertexBuffer weightsHW = new VertexBuffer(Type.HWBoneWeight); + VertexBuffer indicesHW = new VertexBuffer(Type.HWBoneIndex); + indicesHW.setUsage(Usage.CpuOnly); + weightsHW.setUsage(Usage.CpuOnly); + box.setBuffer(weightsHW); + box.setBuffer(indicesHW); + // Setup bone weight buffer FloatBuffer weights = FloatBuffer.allocate( box.getVertexCount() * 4 ); VertexBuffer weightsBuf = new VertexBuffer(Type.BoneWeight); From 500f57a64fb3ad75820120f5115bc7f14c3548a6 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Mon, 14 Sep 2015 23:19:43 -0400 Subject: [PATCH 56/94] AssetManager: set locator path only once per thread --- .../main/java/com/jme3/asset/ImplHandler.java | 21 ++++++----- .../java/jme3test/asset/TestOnlineJar.java | 37 +++++++++++-------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/asset/ImplHandler.java b/jme3-core/src/main/java/com/jme3/asset/ImplHandler.java index 73e6d8df3..3bbd19c14 100644 --- a/jme3-core/src/main/java/com/jme3/asset/ImplHandler.java +++ b/jme3-core/src/main/java/com/jme3/asset/ImplHandler.java @@ -75,7 +75,7 @@ final class ImplHandler { this.assetManager = assetManager; } - protected static class ImplThreadLocal extends ThreadLocal { + protected static class ImplThreadLocal extends ThreadLocal { private final Class type; private final String path; @@ -112,9 +112,13 @@ final class ImplHandler { } @Override - protected Object initialValue(){ + protected T initialValue(){ try { - return type.newInstance(); + T obj = type.newInstance(); + if (path != null) { + ((AssetLocator)obj).setRootPath(path); + } + return obj; } catch (InstantiationException ex) { logger.log(Level.SEVERE,"Cannot create locator of type {0}, does" + " the class have an empty and publically accessible"+ @@ -169,14 +173,11 @@ final class ImplHandler { return null; } - for (ImplThreadLocal local : locatorsList){ - AssetLocator locator = (AssetLocator) local.get(); - if (local.getPath() != null){ - locator.setRootPath((String) local.getPath()); - } - AssetInfo info = locator.locate(assetManager, key); - if (info != null) + for (ImplThreadLocal local : locatorsList){ + AssetInfo info = local.get().locate(assetManager, key); + if (info != null) { return info; + } } return null; diff --git a/jme3-examples/src/main/java/jme3test/asset/TestOnlineJar.java b/jme3-examples/src/main/java/jme3test/asset/TestOnlineJar.java index e12769cd8..036d9318c 100644 --- a/jme3-examples/src/main/java/jme3test/asset/TestOnlineJar.java +++ b/jme3-examples/src/main/java/jme3test/asset/TestOnlineJar.java @@ -36,10 +36,10 @@ import com.jme3.app.SimpleApplication; import com.jme3.asset.TextureKey; import com.jme3.asset.plugins.HttpZipLocator; import com.jme3.material.Material; -import com.jme3.math.Vector3f; import com.jme3.scene.Geometry; import com.jme3.scene.shape.Quad; import com.jme3.texture.Texture; +import com.jme3.ui.Picture; /** * This tests loading a file from a jar stored online. @@ -59,22 +59,27 @@ public class TestOnlineJar extends SimpleApplication { quadMesh.updateGeometry(1, 1, true); Geometry quad = new Geometry("Textured Quad", quadMesh); - assetManager.registerLocator("http://jmonkeyengine.googlecode.com/files/town.zip", - HttpZipLocator.class); + + assetManager.registerLocator("http://jmonkeyengine.googlecode.com/files/town.zip", + HttpZipLocator.class); + assetManager.registerLocator("http://jmonkeyengine.googlecode.com/files/wildhouse.zip", + HttpZipLocator.class); - TextureKey key = new TextureKey("grass.jpg", false); - key.setGenerateMips(true); - Texture tex = assetManager.loadTexture(key); - - Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setTexture("ColorMap", tex); - quad.setMaterial(mat); - - float aspect = tex.getImage().getWidth() / (float) tex.getImage().getHeight(); - quad.setLocalScale(new Vector3f(aspect * 1.5f, 1.5f, 1)); - quad.center(); - - rootNode.attachChild(quad); + Picture pic1 = new Picture("Picture1"); + pic1.move(0, 0, -1); + pic1.setPosition(0, 0); + pic1.setWidth(128); + pic1.setHeight(128); + pic1.setImage(assetManager, "grass.jpg", false); + guiNode.attachChild(pic1); + + Picture pic2 = new Picture("Picture1"); + pic2.move(0, 0, -1); + pic2.setPosition(128, 0); + pic2.setWidth(128); + pic2.setHeight(128); + pic2.setImage(assetManager, "glasstile2.png", false); + guiNode.attachChild(pic2); } } From 30cdca7ad727bc79fe1946fb80ee8375d3938350 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 17 Sep 2015 11:59:44 +0100 Subject: [PATCH 57/94] Native library loading is back to the way it used to be with the addition of LWJGL 3.x libraries added with a different key. Moved LWJGL 3.x repository definition to build.grade in that module. Fixed an issue where frame rate limit would cause GLFW frequency window hint to be set rather than use a software limiter. Removed LWJGLTimer for lwjgl3 module, no need for it any more, we'll just use the NanoTimer. Removed LWJGLCanvas for lwjgl3 module, can't implement this so we'll leave it for now. --- common.gradle | 3 - .../com/jme3/system/NativeLibraryLoader.java | 36 ++ .../system/lwjgl/LwjglAbstractDisplay.java | 1 - .../com/jme3/system/lwjgl/LwjglContext.java | 21 +- jme3-lwjgl3/build.gradle | 6 + .../com/jme3/system/lwjgl/LwjglCanvas.java | 369 ------------------ .../com/jme3/system/lwjgl/LwjglContext.java | 25 +- .../system/lwjgl/LwjglSmoothingTimer.java | 203 ---------- .../com/jme3/system/lwjgl/LwjglTimer.java | 139 ------- .../com/jme3/system/lwjgl/LwjglWindow.java | 51 ++- 10 files changed, 83 insertions(+), 771 deletions(-) delete mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java delete mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java delete mode 100644 jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java diff --git a/common.gradle b/common.gradle index c225588c4..237ee5e7f 100644 --- a/common.gradle +++ b/common.gradle @@ -16,9 +16,6 @@ repositories { maven { url "http://nifty-gui.sourceforge.net/nifty-maven-repo" } - maven { - url "https://oss.sonatype.org/content/repositories/snapshots" - } } configurations { diff --git a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java index 6e83e0e6b..53898883c 100644 --- a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java +++ b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java @@ -124,6 +124,42 @@ public final class NativeLibraryLoader { } static { + // LWJGL + registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/lwjgl.dll"); + registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/lwjgl64.dll"); + registerNativeLibrary("lwjgl", Platform.Linux32, "native/linux/liblwjgl.so"); + registerNativeLibrary("lwjgl", Platform.Linux64, "native/linux/liblwjgl64.so"); + registerNativeLibrary("lwjgl", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); + registerNativeLibrary("lwjgl", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); + + // OpenAL + // For OSX: Need to add lib prefix when extracting + registerNativeLibrary("openal", Platform.Windows32, "native/windows/OpenAL32.dll"); + registerNativeLibrary("openal", Platform.Windows64, "native/windows/OpenAL64.dll"); + registerNativeLibrary("openal", Platform.Linux32, "native/linux/libopenal.so"); + registerNativeLibrary("openal", Platform.Linux64, "native/linux/libopenal64.so"); + registerNativeLibrary("openal", Platform.MacOSX32, "native/macosx/openal.dylib", "libopenal.dylib"); + registerNativeLibrary("openal", Platform.MacOSX64, "native/macosx/openal.dylib", "libopenal.dylib"); + + // LWJGL 3.x + registerNativeLibrary("lwjgl3", Platform.Windows32, "native/windows/lwjgl32.dll"); + registerNativeLibrary("lwjgl3", Platform.Windows64, "native/windows/lwjgl.dll"); + registerNativeLibrary("lwjgl3", Platform.Linux32, "native/linux/liblwjgl32.so"); + registerNativeLibrary("lwjgl3", Platform.Linux64, "native/linux/liblwjgl.so"); + registerNativeLibrary("lwjgl3", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); + registerNativeLibrary("lwjgl3", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); + registerNativeLibrary("lwjgl3", Platform.Windows32, "native/windows/jemalloc32.dll"); + registerNativeLibrary("lwjgl3", Platform.Windows64, "native/windows/jemalloc.dll"); + + // OpenAL for LWJGL 3.x + // For OSX: Need to add lib prefix when extracting + registerNativeLibrary("openal-lwjgl3", Platform.Windows32, "native/windows/OpenAL32.dll"); + registerNativeLibrary("openal-lwjgl3", Platform.Windows64, "native/windows/OpenAL.dll"); + registerNativeLibrary("openal-lwjgl3", Platform.Linux32, "native/linux/libopenal32.so"); + registerNativeLibrary("openal-lwjgl3", Platform.Linux64, "native/linux/libopenal.so"); + registerNativeLibrary("openal-lwjgl3", Platform.MacOSX32, "native/macosx/openal.dylib", "libopenal.dylib"); + registerNativeLibrary("openal-lwjgl3", Platform.MacOSX64, "native/macosx/openal.dylib", "libopenal.dylib"); + // BulletJme registerNativeLibrary("bulletjme", Platform.Windows32, "native/windows/x86/bulletjme.dll"); registerNativeLibrary("bulletjme", Platform.Windows64, "native/windows/x86_64/bulletjme.dll"); diff --git a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java index 415084788..802b12a0f 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java +++ b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java @@ -206,7 +206,6 @@ public abstract class LwjglAbstractDisplay extends LwjglContext implements Runna + "Must set with JmeContext.setSystemListner()."); } - registerNatives(); loadNatives(); logger.log(Level.FINE, "Using LWJGL {0}", Sys.getVersion()); if (!initInThread()) { diff --git a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java index 70885c39b..ee12d1fea 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java +++ b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -162,26 +162,7 @@ public abstract class LwjglContext implements JmeContext { } } } - - protected void registerNatives() { - // LWJGL - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/lwjgl.dll"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/lwjgl64.dll"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux32, "native/linux/liblwjgl.so"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux64, "native/linux/liblwjgl64.so"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); - - // For OSX: Need to add lib prefix when extracting - NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows32, "native/windows/OpenAL32.dll"); - NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows64, "native/windows/OpenAL64.dll"); - NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux32, "native/linux/libopenal.so"); - NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux64, "native/linux/libopenal64.so"); - NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX32, "native/macosx/openal.dylib", "libopenal.dylib"); - NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX64, "native/macosx/openal.dylib", "libopenal.dylib"); - } - - protected void loadNatives() { + protected void loadNatives() { if (JmeSystem.isLowPermissions()) { return; } diff --git a/jme3-lwjgl3/build.gradle b/jme3-lwjgl3/build.gradle index c9b43e0de..3c7e4e646 100644 --- a/jme3-lwjgl3/build.gradle +++ b/jme3-lwjgl3/build.gradle @@ -2,6 +2,12 @@ if (!hasProperty('mainClass')) { ext.mainClass = '' } +repositories { + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + } +} + dependencies { compile project(':jme3-core') compile project(':jme3-desktop') diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java deleted file mode 100644 index 1f1ef2ab5..000000000 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright (c) 2009-2012 jMonkeyEngine - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of 'jMonkeyEngine' nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package com.jme3.system.lwjgl; - -import com.jme3.system.AppSettings; -import com.jme3.system.JmeCanvasContext; - -import javax.swing.*; -import java.awt.*; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static org.lwjgl.glfw.GLFW.glfwDestroyWindow; - -public class LwjglCanvas extends LwjglWindow implements JmeCanvasContext { - - protected static final int TASK_NOTHING = 0, - TASK_DESTROY_DISPLAY = 1, - TASK_CREATE_DISPLAY = 2, - TASK_COMPLETE = 3; - -// protected static final boolean USE_SHARED_CONTEXT = -// Boolean.parseBoolean(System.getProperty("jme3.canvas.sharedctx", "true")); - - protected static final boolean USE_SHARED_CONTEXT = false; - - private static final Logger logger = Logger.getLogger(LwjglCanvas.class.getName()); - private Canvas canvas; - private int width; - private int height; - - private final Object taskLock = new Object(); - private int desiredTask = TASK_NOTHING; - - private Thread renderThread; - private boolean runningFirstTime = true; - private boolean mouseWasGrabbed = false; - - private boolean mouseWasCreated = false; - private boolean keyboardWasCreated = false; - - private long window; - - private class GLCanvas extends Canvas { - @Override - public void addNotify(){ - super.addNotify(); - - if (renderThread != null && renderThread.getState() == Thread.State.TERMINATED) { - return; // already destroyed. - } - - if (renderThread == null){ - logger.log(Level.FINE, "EDT: Creating OGL thread."); - - // Also set some settings on the canvas here. - // So we don't do it outside the AWT thread. - canvas.setFocusable(true); - canvas.setIgnoreRepaint(true); - - renderThread = new Thread(LwjglCanvas.this, THREAD_NAME); - renderThread.start(); - }else if (needClose.get()){ - return; - } - - logger.log(Level.FINE, "EDT: Telling OGL to create display .."); - synchronized (taskLock){ - desiredTask = TASK_CREATE_DISPLAY; -// while (desiredTask != TASK_COMPLETE){ -// try { -// taskLock.wait(); -// } catch (InterruptedException ex) { -// return; -// } -// } -// desiredTask = TASK_NOTHING; - } -// logger.log(Level.FINE, "EDT: OGL has created the display"); - } - - @Override - public void removeNotify(){ - if (needClose.get()){ - logger.log(Level.FINE, "EDT: Application is stopped. Not restoring canvas."); - super.removeNotify(); - return; - } - - // We must tell GL context to shutdown and wait for it to - // shutdown, otherwise, issues will occur. - logger.log(Level.FINE, "EDT: Telling OGL to destroy display .."); - synchronized (taskLock){ - desiredTask = TASK_DESTROY_DISPLAY; - while (desiredTask != TASK_COMPLETE){ - try { - taskLock.wait(); - } catch (InterruptedException ex){ - super.removeNotify(); - return; - } - } - desiredTask = TASK_NOTHING; - } - - logger.log(Level.FINE, "EDT: Acknowledged receipt of canvas death"); - // GL context is dead at this point - - super.removeNotify(); - } - } - - public LwjglCanvas(){ - super(Type.Canvas); - canvas = new GLCanvas(); - } - - public void create(boolean waitFor){ - if (renderThread == null){ - logger.log(Level.FINE, "MAIN: Creating OGL thread."); - - renderThread = new Thread(LwjglCanvas.this, THREAD_NAME); - renderThread.start(); - } - // do not do anything. - // superclass's create() will be called at initInThread() - if (waitFor) { - waitFor(true); - } - } - - public Canvas getCanvas(){ - return canvas; - } - - @Override - protected void runLoop(){ - if (desiredTask != TASK_NOTHING){ - synchronized (taskLock){ - switch (desiredTask){ - case TASK_CREATE_DISPLAY: - logger.log(Level.FINE, "OGL: Creating display .."); - restoreCanvas(); - listener.gainFocus(); - desiredTask = TASK_NOTHING; - break; - case TASK_DESTROY_DISPLAY: - logger.log(Level.FINE, "OGL: Destroying display .."); - listener.loseFocus(); - pauseCanvas(); - break; - } - desiredTask = TASK_COMPLETE; - taskLock.notifyAll(); - } - } - - if (renderable.get()){ - int newWidth = Math.max(canvas.getWidth(), 1); - int newHeight = Math.max(canvas.getHeight(), 1); - - if (width != newWidth || height != newHeight){ - width = newWidth; - height = newHeight; - if (listener != null){ - listener.reshape(width, height); - } - } - } - - super.runLoop(); - } - - private void pauseCanvas(){ - if (mouseInput != null) { - mouseInput.setCursorVisible(true); - mouseWasCreated = true; - } - -/* - if (Mouse.isCreated()){ - if (Mouse.isGrabbed()){ - Mouse.setGrabbed(false); - mouseWasGrabbed = true; - } - mouseWasCreated = true; - Mouse.destroy(); - } - if (Keyboard.isCreated()){ - keyboardWasCreated = true; - Keyboard.destroy(); - } -*/ - - renderable.set(false); - destroyContext(); - } - - /** - * Called to restore the canvas. - */ - private void restoreCanvas(){ - logger.log(Level.FINE, "OGL: Waiting for canvas to become displayable.."); - while (!canvas.isDisplayable()){ - try { - Thread.sleep(10); - } catch (InterruptedException ex) { - logger.log(Level.SEVERE, "OGL: Interrupted! ", ex); - } - } - - logger.log(Level.FINE, "OGL: Creating display context .."); - - // Set renderable to true, since canvas is now displayable. - renderable.set(true); - createContext(settings); - - logger.log(Level.FINE, "OGL: Display is active!"); - - try { - if (mouseWasCreated){ -// Mouse.create(); -// if (mouseWasGrabbed){ -// Mouse.setGrabbed(true); -// mouseWasGrabbed = false; -// } - } - if (keyboardWasCreated){ -// Keyboard.create(); -// keyboardWasCreated = false; - } - } catch (Exception ex){ - logger.log(Level.SEVERE, "Encountered exception when restoring input", ex); - } - - SwingUtilities.invokeLater(new Runnable(){ - public void run(){ - canvas.requestFocus(); - } - }); - } - -/* - */ -/** - * Makes sure the pbuffer is available and ready for use - *//* - - protected void makePbufferAvailable() throws LWJGLException{ - if (pbuffer != null && pbuffer.isBufferLost()){ - logger.log(Level.WARNING, "PBuffer was lost!"); - pbuffer.destroy(); - pbuffer = null; - } - - if (pbuffer == null) { - pbuffer = new Pbuffer(1, 1, acquirePixelFormat(true), null); - pbuffer.makeCurrent(); - logger.log(Level.FINE, "OGL: Pbuffer has been created"); - - // Any created objects are no longer valid - if (!runningFirstTime){ - renderer.resetGLObjects(); - } - } - - pbuffer.makeCurrent(); - if (!pbuffer.isCurrent()){ - throw new LWJGLException("Pbuffer cannot be made current"); - } - } - - protected void destroyPbuffer(){ - if (pbuffer != null){ - if (!pbuffer.isBufferLost()){ - pbuffer.destroy(); - } - pbuffer = null; - } - } -*/ - - /** - * This is called: - * 1) When the context thread ends - * 2) Any time the canvas becomes non-displayable - */ - protected void destroyContext(){ - // invalidate the state so renderer can resume operation - if (!USE_SHARED_CONTEXT){ - renderer.cleanup(); - } - - if (window != 0) { - glfwDestroyWindow(window); - } - - // TODO: Destroy input - - - // The canvas is no longer visible, - // but the context thread is still running. - if (!needClose.get()){ - renderer.invalidateState(); - } - } - - /** - * This is called: - * 1) When the context thread starts - * 2) Any time the canvas becomes displayable again. - */ - @Override - protected void createContext(final AppSettings settings) { - // In case canvas is not visible, we still take framerate - // from settings to prevent "100% CPU usage" - allowSwapBuffers = settings.isSwapBuffers(); - - if (renderable.get()){ - if (!runningFirstTime){ - // because the display is a different opengl context - // must reset the context state. - if (!USE_SHARED_CONTEXT){ - renderer.cleanup(); - } - } - - super.createContext(settings); - } - - // At this point, the OpenGL context is active. - if (runningFirstTime) { - // THIS is the part that creates the renderer. - // It must always be called, now that we have the pbuffer workaround. - initContextFirstTime(); - runningFirstTime = false; - } - } -} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java index 86b6a294a..7f62a447e 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -98,41 +98,20 @@ public abstract class LwjglContext implements JmeContext { return Integer.MAX_VALUE; } - protected void registerNatives() { - // LWJGL - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/lwjgl32.dll"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/lwjgl.dll"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux32, "native/linux/liblwjgl32.so"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Linux64, "native/linux/liblwjgl.so"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows32, "native/windows/jemalloc32.dll"); - NativeLibraryLoader.registerNativeLibrary("lwjgl", Platform.Windows64, "native/windows/jemalloc.dll"); - - // OpenAL - // For OSX: Need to add lib prefix when extracting - NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows32, "native/windows/OpenAL32.dll"); - NativeLibraryLoader.registerNativeLibrary("openal", Platform.Windows64, "native/windows/OpenAL.dll"); - NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux32, "native/linux/libopenal32.so"); - NativeLibraryLoader.registerNativeLibrary("openal", Platform.Linux64, "native/linux/libopenal.so"); - NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX32, "native/macosx/openal.dylib", "libopenal.dylib"); - NativeLibraryLoader.registerNativeLibrary("openal", Platform.MacOSX64, "native/macosx/openal.dylib", "libopenal.dylib"); - } - protected void loadNatives() { if (JmeSystem.isLowPermissions()) { return; } if ("LWJGL".equals(settings.getAudioRenderer())) { - NativeLibraryLoader.loadNativeLibrary("openal", true); + NativeLibraryLoader.loadNativeLibrary("openal-lwjgl3", true); } if (NativeLibraryLoader.isUsingNativeBullet()) { NativeLibraryLoader.loadNativeLibrary("bulletjme", true); } - NativeLibraryLoader.loadNativeLibrary("lwjgl", true); + NativeLibraryLoader.loadNativeLibrary("lwjgl3", true); } protected int getNumSamplesToUse() { diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java deleted file mode 100644 index a7960a261..000000000 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglSmoothingTimer.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (c) 2009-2012 jMonkeyEngine - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of 'jMonkeyEngine' nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package com.jme3.system.lwjgl; - -import com.jme3.math.FastMath; -import com.jme3.system.Timer; -import org.lwjgl.glfw.GLFW; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Timer handles the system's time related functionality. This - * allows the calculation of the framerate. To keep the framerate calculation - * accurate, a call to update each frame is required. Timer is a - * singleton object and must be created via the getTimer method. - * - * @author Mark Powell - * @version $Id: LWJGLTimer.java,v 1.21 2007/09/22 16:46:35 irrisor Exp $ - */ -public class LwjglSmoothingTimer extends LwjglTimer { - private static final Logger logger = Logger.getLogger(LwjglSmoothingTimer.class - .getName()); - - private long lastFrameDiff; - - //frame rate parameters. - private long oldTime; - - private float lastTPF, lastFPS; - - public static int TIMER_SMOOTHNESS = 32; - - private long[] tpf; - - private int smoothIndex; - - private final static long LWJGL_TIMER_RES = 1; - private final static float INV_LWJGL_TIMER_RES = ( 1f / LWJGL_TIMER_RES ); - private static float invTimerRezSmooth; - - public final static long LWJGL_TIME_TO_NANOS = (1000000000 / LWJGL_TIMER_RES); - - private long startTime; - - private boolean allSmooth = false; - - /** - * Constructor builds a Timer object. All values will be - * initialized to it's default values. - */ - public LwjglSmoothingTimer() { - reset(); - - //print timer resolution info - logger.log(Level.FINE, "Timer resolution: {0} ticks per second", LWJGL_TIMER_RES); - } - - public void reset() { - lastFrameDiff = 0; - lastFPS = 0; - lastTPF = 0; - - // init to -1 to indicate this is a new timer. - oldTime = -1; - //reset time - startTime = (long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS); - - tpf = new long[TIMER_SMOOTHNESS]; - smoothIndex = TIMER_SMOOTHNESS - 1; - invTimerRezSmooth = ( 1f / (LWJGL_TIMER_RES * TIMER_SMOOTHNESS)); - - // set tpf... -1 values will not be used for calculating the average in update() - for ( int i = tpf.length; --i >= 0; ) { - tpf[i] = -1; - } - } - - /** - * @see Timer#getResolution() - */ - public long getResolution() { - return LWJGL_TIMER_RES; - } - - /** - * getFrameRate returns the current frame rate since the last - * call to update. - * - * @return the current frame rate. - */ - public float getFrameRate() { - return lastFPS; - } - - public float getTimePerFrame() { - return lastTPF; - } - - /** - * update recalulates the frame rate based on the previous - * call to update. It is assumed that update is called each frame. - */ - public void update() { - long newTime = (long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS); - long oldTime = this.oldTime; - this.oldTime = newTime; - if ( oldTime == -1 ) { - // For the first frame use 60 fps. This value will not be counted in further averages. - // This is done so initialization code between creating the timer and the first - // frame is not counted as a single frame on it's own. - lastTPF = 1 / 60f; - lastFPS = 1f / lastTPF; - return; - } - - long frameDiff = newTime - oldTime; - long lastFrameDiff = this.lastFrameDiff; - if ( lastFrameDiff > 0 && frameDiff > lastFrameDiff *100 ) { - frameDiff = lastFrameDiff *100; - } - this.lastFrameDiff = frameDiff; - tpf[smoothIndex] = frameDiff; - smoothIndex--; - if ( smoothIndex < 0 ) { - smoothIndex = tpf.length - 1; - } - - lastTPF = 0.0f; - if (!allSmooth) { - int smoothCount = 0; - for ( int i = tpf.length; --i >= 0; ) { - if ( tpf[i] != -1 ) { - lastTPF += tpf[i]; - smoothCount++; - } - } - if (smoothCount == tpf.length) - allSmooth = true; - lastTPF *= ( INV_LWJGL_TIMER_RES / smoothCount ); - } else { - for ( int i = tpf.length; --i >= 0; ) { - if ( tpf[i] != -1 ) { - lastTPF += tpf[i]; - } - } - lastTPF *= invTimerRezSmooth; - } - if ( lastTPF < FastMath.FLT_EPSILON ) { - lastTPF = FastMath.FLT_EPSILON; - } - - lastFPS = 1f / lastTPF; - } - - /** - * toString returns the string representation of this timer - * in the format:
- *
- * jme.utility.Timer@1db699b
- * Time: {LONG}
- * FPS: {LONG}
- * - * @return the string representation of this object. - */ - @Override - public String toString() { - String string = super.toString(); - string += "\nTime: " + oldTime; - string += "\nFPS: " + getFrameRate(); - return string; - } -} \ No newline at end of file diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java deleted file mode 100644 index c4a0e5025..000000000 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglTimer.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (c) 2009-2012 jMonkeyEngine - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of 'jMonkeyEngine' nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package com.jme3.system.lwjgl; - -import com.jme3.system.Timer; -import org.lwjgl.glfw.GLFW; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Timer handles the system's time related functionality. This - * allows the calculation of the framerate. To keep the framerate calculation - * accurate, a call to update each frame is required. Timer is a - * singleton object and must be created via the getTimer method. - * - * @author Mark Powell - * @version $Id: LWJGLTimer.java,v 1.21 2007/09/22 16:46:35 irrisor Exp $ - */ -public class LwjglTimer extends Timer { - - private static final Logger logger = Logger.getLogger(LwjglTimer.class.getName()); - - //frame rate parameters. - private long oldTime; - private long startTime; - - private float lastTPF, lastFPS; - - private final static long LWJGL_TIMER_RES = 1; - private final static float INV_LWJGL_TIMER_RES = ( 1f / LWJGL_TIMER_RES ); - public final static long LWJGL_TIME_TO_NANOS = (1000000000 / LWJGL_TIMER_RES); - - /** - * Constructor builds a Timer object. All values will be - * initialized to it's default values. - */ - public LwjglTimer() { - reset(); - logger.log(Level.FINE, "Timer resolution: {0} ticks per second", LWJGL_TIMER_RES); - } - - public void reset() { - startTime = (long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS); - oldTime = getTime(); - } - - @Override - public float getTimeInSeconds() { - return getTime() * INV_LWJGL_TIMER_RES; - } - - /** - * @see Timer#getTime() - */ - public long getTime() { - return ((long) (GLFW.glfwGetTime() * LWJGL_TIME_TO_NANOS) - startTime); - } - - /** - * @see Timer#getResolution() - */ - public long getResolution() { - return LWJGL_TIMER_RES; - } - - /** - * getFrameRate returns the current frame rate since the last - * call to update. - * - * @return the current frame rate. - */ - public float getFrameRate() { - return lastFPS; - } - - public float getTimePerFrame() { - return lastTPF; - } - - /** - * update recalulates the frame rate based on the previous - * call to update. It is assumed that update is called each frame. - */ - public void update() { - long curTime = getTime(); - lastTPF = (curTime - oldTime) * (1.0f / LWJGL_TIMER_RES); - lastFPS = 1.0f / lastTPF; - oldTime = curTime; - } - - /** - * toString returns the string representation of this timer - * in the format:
- *
- * jme.utility.Timer@1db699b
- * Time: {LONG}
- * FPS: {LONG}
- * - * @return the string representation of this object. - */ - @Override - public String toString() { - String string = super.toString(); - string += "\nTime: " + oldTime; - string += "\nFPS: " + getFrameRate(); - return string; - } -} \ No newline at end of file diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java index c5cb35e03..2f00eb700 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java @@ -42,6 +42,7 @@ import com.jme3.input.lwjgl.GlfwMouseInput; import com.jme3.system.AppSettings; import com.jme3.system.JmeContext; import com.jme3.system.JmeSystem; +import com.jme3.system.NanoTimer; import org.lwjgl.Sys; import org.lwjgl.glfw.*; @@ -73,6 +74,8 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { protected boolean allowSwapBuffers = false; private long window = -1; private final JmeContext.Type type; + private int frameRateLimit = -1; + private double frameSleepTime; private GLFWErrorCallback errorCallback; private GLFWWindowSizeCallback windowSizeCallback; @@ -160,16 +163,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { glfwWindowHint(GLFW_STENCIL_BITS, settings.getStencilBits()); glfwWindowHint(GLFW_SAMPLES, settings.getSamples()); glfwWindowHint(GLFW_STEREO, settings.useStereo3D() ? GL_TRUE : GL_FALSE); - - int frameRateCap = settings.getFrameRate(); - - if (!autoFlush) { - frameRateCap = 20; - } - - if (frameRateCap > 0) { - glfwWindowHint(GLFW_REFRESH_RATE, frameRateCap); - } + glfwWindowHint(GLFW_REFRESH_RATE, settings.getFrequency()); // Not sure how else to support bits per pixel if (settings.getBitsPerPixel() == 24) { @@ -289,7 +283,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { }); } - timer = new LwjglTimer(); + timer = new NanoTimer(); // For canvas, this will create a pbuffer, // allowing us to query information. @@ -303,7 +297,6 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { } catch (Exception ex) { try { if (window != -1) { - //glfwSetWindowShouldClose(window, GL_TRUE); glfwDestroyWindow(window); } } catch (Exception ex2) { @@ -360,11 +353,41 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { if (renderer != null) { renderer.postFrame(); } + + if (autoFlush) { + if (frameRateLimit != getSettings().getFrameRate()) { + setFrameRateLimit(getSettings().getFrameRate()); + } + } else if (frameRateLimit != 20) { + setFrameRateLimit(20); + } + + // If software frame rate limiting has been asked for, lets calculate sleep time based on a base value calculated + // from 1000 / frameRateLimit in milliseconds subtracting the time it has taken to render last frame. + // This gives an approximate limit within 3 fps of the given frame rate limit. + if (frameRateLimit > 0) { + final double sleep = frameSleepTime - (timer.getTimePerFrame() / 1000.0); + final long sleepMillis = (long) sleep; + final int additionalNanos = (int) ((sleep - sleepMillis) * 1000000.0); + + if (sleepMillis >= 0 && additionalNanos >= 0) { + try { + Thread.sleep(sleepMillis, additionalNanos); + } catch (InterruptedException ignored) { + } + } + } + } + + private void setFrameRateLimit(int frameRateLimit) { + this.frameRateLimit = frameRateLimit; + frameSleepTime = 1000.0 / this.frameRateLimit; } /** * De-initialize in the OpenGL thread. */ + protected void deinitInThread() { destroyContext(); @@ -379,7 +402,6 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { + "Must set with JmeContext.setSystemListener()."); } - registerNatives(); loadNatives(); LOGGER.log(Level.FINE, "Using LWJGL {0}", Sys.getVersion()); @@ -445,6 +467,9 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { return window; } + + // TODO: Implement support for window icon when GLFW supports it. + private ByteBuffer[] imagesToByteBuffers(Object[] images) { ByteBuffer[] out = new ByteBuffer[images.length]; for (int i = 0; i < images.length; i++) { From c375974a9a88d9484122e358c89b7ea2db5fd98b Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 17 Sep 2015 14:48:02 +0100 Subject: [PATCH 58/94] Fixed a compiler error where glfw input classes wanted to use the old LwjglTimer class. --- .../src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java | 3 +-- .../src/main/java/com/jme3/input/lwjgl/GlfwMouseInput.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java index 5b7d8e267..20cd07672 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java @@ -35,7 +35,6 @@ package com.jme3.input.lwjgl; import com.jme3.input.KeyInput; import com.jme3.input.RawInputListener; import com.jme3.input.event.KeyInputEvent; -import com.jme3.system.lwjgl.LwjglTimer; import com.jme3.system.lwjgl.LwjglWindow; import org.lwjgl.glfw.GLFWKeyCallback; @@ -112,6 +111,6 @@ public class GlfwKeyInput implements KeyInput { } public long getInputTimeNanos() { - return (long) (glfwGetTime() * LwjglTimer.LWJGL_TIME_TO_NANOS); + return (long) (glfwGetTime() * 1000000000); } } diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwMouseInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwMouseInput.java index 41e76a29d..05a2df5db 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwMouseInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwMouseInput.java @@ -37,7 +37,6 @@ import com.jme3.input.MouseInput; import com.jme3.input.RawInputListener; import com.jme3.input.event.MouseButtonEvent; import com.jme3.input.event.MouseMotionEvent; -import com.jme3.system.lwjgl.LwjglTimer; import com.jme3.system.lwjgl.LwjglWindow; import org.lwjgl.glfw.GLFWCursorPosCallback; import org.lwjgl.glfw.GLFWMouseButtonCallback; @@ -183,7 +182,7 @@ public class GlfwMouseInput implements MouseInput { } public long getInputTimeNanos() { - return (long) (glfwGetTime() * LwjglTimer.LWJGL_TIME_TO_NANOS); + return (long) (glfwGetTime() * 1000000000); } public void setNativeCursor(final JmeCursor jmeCursor) { From 495e0cf6d5603a871aa5b604b068845c9f370b70 Mon Sep 17 00:00:00 2001 From: Georg Date: Mon, 21 Sep 2015 14:28:54 +0200 Subject: [PATCH 59/94] Fixed wrong BoundingBox transformation, when using negative scaling values --- jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java index 5baf3e3d6..c087950a3 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java +++ b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java @@ -314,7 +314,7 @@ public class BoundingBox extends BoundingVolume { transMatrix.absoluteLocal(); Vector3f scale = trans.getScale(); - vars.vect1.set(xExtent * scale.x, yExtent * scale.y, zExtent * scale.z); + vars.vect1.set(xExtent * FastMath.abs(scale.x), yExtent * FastMath.abs(scale.y), zExtent * FastMath.abs(scale.z)); transMatrix.mult(vars.vect1, vars.vect2); // Assign the biggest rotations after scales. box.xExtent = FastMath.abs(vars.vect2.getX()); From e3a45755c64a8624c04963c57271d915deb2c6e8 Mon Sep 17 00:00:00 2001 From: Julien Gouesse Date: Tue, 22 Sep 2015 22:50:31 +0200 Subject: [PATCH 60/94] The JOGL backend doesn't need null-terminated strings for the attributes and the uniforms --- .../src/main/java/com/jme3/renderer/jogl/JoglGL.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL.java b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL.java index 6f335e960..e1c2a2505 100644 --- a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL.java +++ b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL.java @@ -519,14 +519,14 @@ public class JoglGL implements GL, GL2, GL3, GL4 { @Override public int glGetAttribLocation(int param1, String param2) { - // FIXME: Does JOGL require null-terminated strings????? - return GLContext.getCurrentGL().getGL2ES2().glGetAttribLocation(param1, param2 + "\0"); + // JOGL 2.0 doesn't need a null-terminated string + return GLContext.getCurrentGL().getGL2ES2().glGetAttribLocation(param1, param2); } @Override public int glGetUniformLocation(int param1, String param2) { - // FIXME: Does JOGL require null-terminated strings???????? - return GLContext.getCurrentGL().getGL2ES2().glGetUniformLocation(param1, param2 + "\0"); + // JOGL 2.0 doesn't need a null-terminated string + return GLContext.getCurrentGL().getGL2ES2().glGetUniformLocation(param1, param2); } @Override From daa18429d1403a6b9f930a9b9678caddb7347d68 Mon Sep 17 00:00:00 2001 From: Julien Gouesse Date: Tue, 22 Sep 2015 23:06:16 +0200 Subject: [PATCH 61/94] Removes the old JOGL renderer and uses the new unified renderer as it works correctly now --- .../com/jme3/renderer/jogl/JoglRenderer.java | 2699 ----------------- .../com/jme3/system/jogl/JoglContext.java | 5 +- 2 files changed, 1 insertion(+), 2703 deletions(-) delete mode 100644 jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglRenderer.java diff --git a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglRenderer.java b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglRenderer.java deleted file mode 100644 index 111719265..000000000 --- a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglRenderer.java +++ /dev/null @@ -1,2699 +0,0 @@ -/* - * Copyright (c) 2009-2012 jMonkeyEngine - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of 'jMonkeyEngine' nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package com.jme3.renderer.jogl; - -import com.jme3.light.LightList; -import com.jme3.material.RenderState; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Matrix4f; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector2f; -import com.jme3.math.Vector3f; -import com.jme3.math.Vector4f; -import com.jme3.renderer.Caps; -import com.jme3.renderer.IDList; -import com.jme3.renderer.RenderContext; -import com.jme3.renderer.Renderer; -import com.jme3.renderer.RendererException; -import com.jme3.renderer.Statistics; -import com.jme3.scene.Mesh; -import com.jme3.scene.Mesh.Mode; -import com.jme3.scene.VertexBuffer; -import com.jme3.scene.VertexBuffer.Type; -import com.jme3.scene.VertexBuffer.Usage; -import com.jme3.shader.Attribute; -import com.jme3.shader.Shader; -import com.jme3.shader.Shader.ShaderSource; -import com.jme3.shader.Uniform; -import com.jme3.texture.FrameBuffer; -import com.jme3.texture.FrameBuffer.RenderBuffer; -import com.jme3.texture.Image; -import com.jme3.texture.Texture; -import com.jme3.texture.Texture.WrapAxis; -import com.jme3.util.BufferUtils; -import com.jme3.util.ListMap; -import com.jme3.util.NativeObjectManager; -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import java.util.EnumSet; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; -import com.jogamp.nativewindow.NativeWindowFactory; -import com.jogamp.opengl.GL; -import com.jogamp.opengl.GL2; -import com.jogamp.opengl.GL2ES1; -import com.jogamp.opengl.GL2ES2; -import com.jogamp.opengl.GL2ES3; -import com.jogamp.opengl.GL2GL3; -import com.jogamp.opengl.GL3; -import com.jogamp.opengl.GLContext; -import jme3tools.converters.MipMapGenerator; -import jme3tools.shader.ShaderDebug; - -public class JoglRenderer implements Renderer { - - private static final Logger logger = Logger.getLogger(JoglRenderer.class.getName()); - private static final boolean VALIDATE_SHADER = false; - private final ByteBuffer nameBuf = BufferUtils.createByteBuffer(250); - private final StringBuilder stringBuf = new StringBuilder(250); - private final IntBuffer intBuf1 = BufferUtils.createIntBuffer(1); - private final IntBuffer intBuf16 = BufferUtils.createIntBuffer(16); - protected FloatBuffer fb16 = BufferUtils.createFloatBuffer(16); - private RenderContext context = new RenderContext(); - private NativeObjectManager objManager = new NativeObjectManager(); - private EnumSet caps = EnumSet.noneOf(Caps.class); - //current state - private Shader boundShader; - private int initialDrawBuf, initialReadBuf; - private int glslVer; - private int vertexTextureUnits; - private int fragTextureUnits; - private int vertexUniforms; - private int fragUniforms; - private int vertexAttribs; - private int maxFBOSamples; - private int maxFBOAttachs; - private int maxMRTFBOAttachs; - private int maxRBSize; - private int maxTexSize; - private int maxCubeTexSize; - private int maxVertCount; - private int maxTriCount; - private int maxColorTexSamples; - private int maxDepthTexSamples; - private FrameBuffer lastFb = null; - private FrameBuffer mainFbOverride = null; - private final Statistics statistics = new Statistics(); - private int vpX, vpY, vpW, vpH; - private int clipX, clipY, clipW, clipH; - private boolean linearizeSrgbImages; - - public JoglRenderer() { - } - - protected void updateNameBuffer() { - int len = stringBuf.length(); - - nameBuf.position(0); - nameBuf.limit(len); - for (int i = 0; i < len; i++) { - nameBuf.put((byte) stringBuf.charAt(i)); - } - - nameBuf.rewind(); - } - - @Override - public Statistics getStatistics() { - return statistics; - } - - @Override - public EnumSet getCaps() { - return caps; - } - - public void initialize() { - GL gl = GLContext.getCurrentGL(); - //logger.log(Level.FINE, "Vendor: {0}", gl.glGetString(GL.GL_VENDOR)); - //logger.log(Level.FINE, "Renderer: {0}", gl.glGetString(GL.GL_RENDERER)); - //logger.log(Level.FINE, "Version: {0}", gl.glGetString(GL.GL_VERSION)); - if (gl.isExtensionAvailable("GL_VERSION_2_0")) { - caps.add(Caps.OpenGL20); - if (gl.isExtensionAvailable("GL_VERSION_2_1")) { - caps.add(Caps.OpenGL21); - if (gl.isExtensionAvailable("GL_VERSION_3_0")) { - caps.add(Caps.OpenGL30); - if (gl.isExtensionAvailable("GL_VERSION_3_1")) { - caps.add(Caps.OpenGL31); - if (gl.isExtensionAvailable("GL_VERSION_3_2")) { - caps.add(Caps.OpenGL32); - } - } - } - } - } - - //workaround, always assume we support GLSL100 - //some cards just don't report this correctly - caps.add(Caps.GLSL100); - - String versionStr = null; - if (caps.contains(Caps.OpenGL20) || gl.isGL2ES2()) { - versionStr = gl.glGetString(GL2ES2.GL_SHADING_LANGUAGE_VERSION); - } - if (versionStr == null || versionStr.equals("")) { - glslVer = -1; - throw new UnsupportedOperationException("GLSL and OpenGL2 is " + - "required for the JOGL " + - "renderer!"); - } - - // Fix issue in TestRenderToMemory when GL_FRONT is the main - // buffer being used. - gl.glGetIntegerv(GL2GL3.GL_DRAW_BUFFER, intBuf1); - initialDrawBuf = intBuf1.get(0); - gl.glGetIntegerv(GL2GL3.GL_READ_BUFFER, intBuf1); - initialReadBuf = intBuf1.get(0); - - // XXX: This has to be GL_BACK for canvas on Mac - // Since initialDrawBuf is GL_FRONT for pbuffer, gotta - // change this value later on ... -// initialDrawBuf = GL_BACK; -// initialReadBuf = GL_BACK; - - int spaceIdx = versionStr.indexOf(" "); - if (spaceIdx >= 1) { - versionStr = versionStr.substring(0, spaceIdx); - } - - try { - float version = Float.parseFloat(versionStr); - glslVer = (int) (version * 100); - } catch (NumberFormatException e) { - // the parsing fails on Raspberry Pi - if (NativeWindowFactory.getNativeWindowType(false).equals(NativeWindowFactory.TYPE_BCM_VC_IV)) { - logger.warning("Failed parsing GLSL version assuming it's v1.00"); - glslVer = 100; - } - } - - switch (glslVer) { - default: - if (glslVer < 400) { - break; - } - - // so that future OpenGL revisions wont break jme3 - - // fall through intentional - case 400: - case 330: - case 150: - caps.add(Caps.GLSL150); - case 140: - caps.add(Caps.GLSL140); - case 130: - caps.add(Caps.GLSL130); - case 120: - caps.add(Caps.GLSL120); - case 110: - caps.add(Caps.GLSL110); - case 100: - caps.add(Caps.GLSL100); - break; - } - - if (!caps.contains(Caps.GLSL100)) { - logger.log(Level.WARNING, "Force-adding GLSL100 support, since OpenGL2 is supported."); - caps.add(Caps.GLSL100); - } - - gl.glGetIntegerv(GL2ES2.GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, intBuf16); - vertexTextureUnits = intBuf16.get(0); - logger.log(Level.FINER, "VTF Units: {0}", vertexTextureUnits); - if (vertexTextureUnits > 0) { - caps.add(Caps.VertexTextureFetch); - } - - gl.glGetIntegerv(GL2ES2.GL_MAX_TEXTURE_IMAGE_UNITS, intBuf16); - fragTextureUnits = intBuf16.get(0); - logger.log(Level.FINER, "Texture Units: {0}", fragTextureUnits); - - gl.glGetIntegerv(GL2GL3.GL_MAX_VERTEX_UNIFORM_COMPONENTS, intBuf16); - vertexUniforms = intBuf16.get(0); - logger.log(Level.FINER, "Vertex Uniforms: {0}", vertexUniforms); - - gl.glGetIntegerv(GL2GL3.GL_MAX_FRAGMENT_UNIFORM_COMPONENTS, intBuf16); - fragUniforms = intBuf16.get(0); - logger.log(Level.FINER, "Fragment Uniforms: {0}", fragUniforms); - - gl.glGetIntegerv(GL2ES2.GL_MAX_VERTEX_ATTRIBS, intBuf16); - vertexAttribs = intBuf16.get(0); - logger.log(Level.FINER, "Vertex Attributes: {0}", vertexAttribs); - - gl.glGetIntegerv(GL2GL3.GL_MAX_VARYING_FLOATS, intBuf16); - int varyingFloats = intBuf16.get(0); - logger.log(Level.FINER, "Varying Floats: {0}", varyingFloats); - - gl.glGetIntegerv(GL.GL_SUBPIXEL_BITS, intBuf16); - int subpixelBits = intBuf16.get(0); - logger.log(Level.FINER, "Subpixel Bits: {0}", subpixelBits); - - gl.glGetIntegerv(GL2GL3.GL_MAX_ELEMENTS_VERTICES, intBuf16); - maxVertCount = intBuf16.get(0); - logger.log(Level.FINER, "Preferred Batch Vertex Count: {0}", maxVertCount); - - gl.glGetIntegerv(GL2GL3.GL_MAX_ELEMENTS_INDICES, intBuf16); - maxTriCount = intBuf16.get(0); - logger.log(Level.FINER, "Preferred Batch Index Count: {0}", maxTriCount); - - gl.glGetIntegerv(GL.GL_MAX_TEXTURE_SIZE, intBuf16); - maxTexSize = intBuf16.get(0); - logger.log(Level.FINER, "Maximum Texture Resolution: {0}", maxTexSize); - - gl.glGetIntegerv(GL.GL_MAX_CUBE_MAP_TEXTURE_SIZE, intBuf16); - maxCubeTexSize = intBuf16.get(0); - logger.log(Level.FINER, "Maximum CubeMap Resolution: {0}", maxCubeTexSize); - - if (gl.isExtensionAvailable("GL_ARB_color_buffer_float")) { - // XXX: Require both 16 and 32 bit float support for FloatColorBuffer. - if (gl.isExtensionAvailable("GL_ARB_half_float_pixel")) { - caps.add(Caps.FloatColorBuffer); - } - } - - if (gl.isExtensionAvailable("GL_ARB_depth_buffer_float")) { - caps.add(Caps.FloatDepthBuffer); - } - - if (caps.contains(Caps.OpenGL30)) { - caps.add(Caps.PackedDepthStencilBuffer); - } - - if (gl.isExtensionAvailable("GL_ARB_draw_instanced") || gl.isExtensionAvailable("GL_ARB_instanced_arrays")) { - caps.add(Caps.MeshInstancing); - } - - if (gl.isExtensionAvailable("GL_ARB_texture_buffer_object")) { - caps.add(Caps.TextureBuffer); - } - - if (gl.isExtensionAvailable("GL_ARB_texture_float")) { - if (gl.isExtensionAvailable("GL_ARB_half_float_pixel")) { - caps.add(Caps.FloatTexture); - } - } - - if (gl.isExtensionAvailable("GL_ARB_vertex_array_object")) { - caps.add(Caps.VertexBufferArray); - } - - if (gl.isExtensionAvailable("GL_ARB_texture_non_power_of_two")) { - caps.add(Caps.NonPowerOfTwoTextures); - } - else { - logger.log(Level.WARNING, "Your graphics card does not " - + "support non-power-of-2 textures. " - + "Some features might not work."); - } - - if (gl.isExtensionAvailable("GL_EXT_packed_float") || gl.isExtensionAvailable("GL_VERSION_3_0")) { - // This format is part of the OGL3 specification - caps.add(Caps.PackedFloatColorBuffer); - if (gl.isExtensionAvailable("GL_ARB_half_float_pixel")) { - // because textures are usually uploaded as RGB16F - // need half-float pixel - caps.add(Caps.PackedFloatTexture); - } - } - - if (gl.isExtensionAvailable("GL_EXT_texture_array") || gl.isExtensionAvailable("GL_VERSION_3_0")) { - caps.add(Caps.TextureArray); - } - - if (gl.isExtensionAvailable("GL_EXT_texture_shared_exponent") || gl.isExtensionAvailable("GL_VERSION_3_0")) { - caps.add(Caps.SharedExponentTexture); - } - - if (gl.isExtensionAvailable("GL_EXT_framebuffer_object")) { - caps.add(Caps.FrameBuffer); - - gl.glGetIntegerv(GL.GL_MAX_RENDERBUFFER_SIZE, intBuf16); - maxRBSize = intBuf16.get(0); - logger.log(Level.FINER, "FBO RB Max Size: {0}", maxRBSize); - - gl.glGetIntegerv(GL2GL3.GL_MAX_COLOR_ATTACHMENTS, intBuf16); - maxFBOAttachs = intBuf16.get(0); - logger.log(Level.FINER, "FBO Max renderbuffers: {0}", maxFBOAttachs); - - if (gl.isExtensionAvailable("GL_EXT_framebuffer_multisample")) { - caps.add(Caps.FrameBufferMultisample); - - gl.glGetIntegerv(GL2GL3.GL_MAX_SAMPLES, intBuf16); - maxFBOSamples = intBuf16.get(0); - logger.log(Level.FINER, "FBO Max Samples: {0}", maxFBOSamples); - } - - if (gl.isExtensionAvailable("GL_ARB_texture_multisample")) { - caps.add(Caps.TextureMultisample); - - gl.glGetIntegerv(GL3.GL_MAX_COLOR_TEXTURE_SAMPLES, intBuf16); - maxColorTexSamples = intBuf16.get(0); - logger.log(Level.FINER, "Texture Multisample Color Samples: {0}", maxColorTexSamples); - - gl.glGetIntegerv(GL3.GL_MAX_DEPTH_TEXTURE_SAMPLES, intBuf16); - maxDepthTexSamples = intBuf16.get(0); - logger.log(Level.FINER, "Texture Multisample Depth Samples: {0}", maxDepthTexSamples); - } - - gl.glGetIntegerv(GL2ES2.GL_MAX_DRAW_BUFFERS, intBuf16); - maxMRTFBOAttachs = intBuf16.get(0); - if (maxMRTFBOAttachs > 1) { - caps.add(Caps.FrameBufferMRT); - logger.log(Level.FINER, "FBO Max MRT renderbuffers: {0}", maxMRTFBOAttachs); - } - - //if (gl.isExtensionAvailable("GL_ARB_draw_buffers")) { - // caps.add(Caps.FrameBufferMRT); - // gl.glGetIntegerv(GL2GL3.GL_MAX_DRAW_BUFFERS, intBuf16); - // maxMRTFBOAttachs = intBuf16.get(0); - // logger.log(Level.FINER, "FBO Max MRT renderbuffers: {0}", maxMRTFBOAttachs); - //} - } - - if (gl.isExtensionAvailable("GL_ARB_multisample")) { - gl.glGetIntegerv(GL.GL_SAMPLE_BUFFERS, intBuf16); - boolean available = intBuf16.get(0) != 0; - gl.glGetIntegerv(GL.GL_SAMPLES, intBuf16); - int samples = intBuf16.get(0); - logger.log(Level.FINER, "Samples: {0}", samples); - boolean enabled = gl.glIsEnabled(GL.GL_MULTISAMPLE); - if (samples > 0 && available && !enabled) { - gl.glEnable(GL.GL_MULTISAMPLE); - } - caps.add(Caps.Multisample); - } - - //supports sRGB pipeline - if ((gl.isExtensionAvailable("GL_ARB_framebuffer_sRGB") && gl.isExtensionAvailable("GL_EXT_texture_sRGB")) || gl.isExtensionAvailable("GL_VERSION_3_0")){ - caps.add(Caps.Srgb); - } - - logger.log(Level.FINE, "Caps: {0}", caps); - } - - @Override - public void invalidateState() { - context.reset(); - boundShader = null; - lastFb = null; - - GL gl = GLContext.getCurrentGL(); - gl.glGetIntegerv(GL2GL3.GL_DRAW_BUFFER, intBuf1); - initialDrawBuf = intBuf1.get(0); - gl.glGetIntegerv(GL2GL3.GL_READ_BUFFER, intBuf1); - initialReadBuf = intBuf1.get(0); - } - - @Override - public void resetGLObjects() { - logger.log(Level.FINE, "Reseting objects and invalidating state"); - objManager.resetObjects(); - statistics.clearMemory(); - invalidateState(); - } - - @Override - public void cleanup() { - logger.log(Level.FINE, "Deleting objects and invalidating state"); - objManager.deleteAllObjects(this); - statistics.clearMemory(); - invalidateState(); - } - -// private void checkCap(Caps cap) { -// if (!caps.contains(cap)) { -// throw new UnsupportedOperationException("Required capability missing: " + cap.name()); -// } -// } - - /*********************************************************************\ - |* Render State *| - \*********************************************************************/ - @Override - public void setDepthRange(float start, float end) { - GL gl = GLContext.getCurrentGL(); - gl.glDepthRange(start, end); - } - - @Override - public void clearBuffers(boolean color, boolean depth, boolean stencil) { - GL gl = GLContext.getCurrentGL(); - int bits = 0; - if (color) { - //See explanations of the depth below, we must enable color write to be able to clear the color buffer - if (context.colorWriteEnabled == false) { - gl.glColorMask(true, true, true, true); - context.colorWriteEnabled = true; - } - bits = GL.GL_COLOR_BUFFER_BIT; - } - if (depth) { - - //glClear(GL_DEPTH_BUFFER_BIT) seems to not work when glDepthMask is false - //here s some link on openl board - //http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=257223 - //if depth clear is requested, we enable the depthMask - if (context.depthWriteEnabled == false) { - gl.glDepthMask(true); - context.depthWriteEnabled = true; - } - bits |= GL.GL_DEPTH_BUFFER_BIT; - } - if (stencil) { - bits |= GL.GL_STENCIL_BUFFER_BIT; - } - if (bits != 0) { - gl.glClear(bits); - } - } - - @Override - public void setBackgroundColor(ColorRGBA color) { - GL gl = GLContext.getCurrentGL(); - gl.glClearColor(color.r, color.g, color.b, color.a); - } - - @Override - public void setAlphaToCoverage(boolean value) { - if (caps.contains(Caps.Multisample)) { - GL gl = GLContext.getCurrentGL(); - if (value) { - gl.glEnable(GL.GL_SAMPLE_ALPHA_TO_COVERAGE); - } else { - gl.glDisable(GL.GL_SAMPLE_ALPHA_TO_COVERAGE); - } - } - } - - @Override - public void applyRenderState(RenderState state) { - GL gl = GLContext.getCurrentGL(); - if (state.isWireframe() && !context.wireframe) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glPolygonMode(GL.GL_FRONT_AND_BACK, GL2GL3.GL_LINE); - } - context.wireframe = true; - } else if (!state.isWireframe() && context.wireframe) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glPolygonMode(GL.GL_FRONT_AND_BACK, GL2GL3.GL_FILL); - } - context.wireframe = false; - } - - if (state.isDepthTest() && !context.depthTestEnabled) { - gl.glEnable(GL.GL_DEPTH_TEST); - gl.glDepthFunc(convertTestFunction(context.depthFunc)); - context.depthTestEnabled = true; - } else if (!state.isDepthTest() && context.depthTestEnabled) { - gl.glDisable(GL.GL_DEPTH_TEST); - context.depthTestEnabled = false; - } - if (state.getDepthFunc() != context.depthFunc) { - gl.glDepthFunc(convertTestFunction(state.getDepthFunc())); - context.depthFunc = state.getDepthFunc(); - } - if (gl.isGL2ES1()) { - if (state.isAlphaTest() && !context.alphaTestEnabled) { - gl.glEnable(GL2ES1.GL_ALPHA_TEST); - gl.getGL2ES1().glAlphaFunc(convertTestFunction(context.alphaFunc), context.alphaTestFallOff); - context.alphaTestEnabled = true; - } else if (!state.isAlphaTest() && context.alphaTestEnabled) { - gl.glDisable(GL2ES1.GL_ALPHA_TEST); - context.alphaTestEnabled = false; - } - if (state.getAlphaFallOff() != context.alphaTestFallOff) { - gl.getGL2ES1().glAlphaFunc(convertTestFunction(context.alphaFunc), context.alphaTestFallOff); - context.alphaTestFallOff = state.getAlphaFallOff(); - } - if (state.getAlphaFunc() != context.alphaFunc) { - gl.getGL2ES1().glAlphaFunc(convertTestFunction(state.getAlphaFunc()), context.alphaTestFallOff); - context.alphaFunc = state.getAlphaFunc(); - } - } - - if (state.isDepthWrite() && !context.depthWriteEnabled) { - gl.glDepthMask(true); - context.depthWriteEnabled = true; - } else if (!state.isDepthWrite() && context.depthWriteEnabled) { - gl.glDepthMask(false); - context.depthWriteEnabled = false; - } - - if (state.isColorWrite() && !context.colorWriteEnabled) { - gl.glColorMask(true, true, true, true); - context.colorWriteEnabled = true; - } else if (!state.isColorWrite() && context.colorWriteEnabled) { - gl.glColorMask(false, false, false, false); - context.colorWriteEnabled = false; - } - - if (state.isPointSprite() && !context.pointSprite) { - // Only enable/disable sprite - if (context.boundTextures[0] != null) { - if (context.boundTextureUnit != 0) { - gl.glActiveTexture(GL.GL_TEXTURE0); - context.boundTextureUnit = 0; - } - if (gl.isGL2ES1()) { - gl.glEnable(GL2ES1.GL_POINT_SPRITE); - } - if (gl.isGL2GL3()) { - gl.glEnable(GL2GL3.GL_VERTEX_PROGRAM_POINT_SIZE); - } - } - context.pointSprite = true; - } else if (!state.isPointSprite() && context.pointSprite) { - if (context.boundTextures[0] != null) { - if (context.boundTextureUnit != 0) { - gl.glActiveTexture(GL.GL_TEXTURE0); - context.boundTextureUnit = 0; - } - if (gl.isGL2ES1()) { - gl.glDisable(GL2ES1.GL_POINT_SPRITE); - } - if (gl.isGL2GL3()) { - gl.glDisable(GL2GL3.GL_VERTEX_PROGRAM_POINT_SIZE); - } - context.pointSprite = false; - } - } - - if (state.isPolyOffset()) { - if (!context.polyOffsetEnabled) { - gl.glEnable(GL.GL_POLYGON_OFFSET_FILL); - gl.glPolygonOffset(state.getPolyOffsetFactor(), - state.getPolyOffsetUnits()); - context.polyOffsetEnabled = true; - context.polyOffsetFactor = state.getPolyOffsetFactor(); - context.polyOffsetUnits = state.getPolyOffsetUnits(); - } else { - if (state.getPolyOffsetFactor() != context.polyOffsetFactor - || state.getPolyOffsetUnits() != context.polyOffsetUnits) { - gl.glPolygonOffset(state.getPolyOffsetFactor(), - state.getPolyOffsetUnits()); - context.polyOffsetFactor = state.getPolyOffsetFactor(); - context.polyOffsetUnits = state.getPolyOffsetUnits(); - } - } - } else { - if (context.polyOffsetEnabled) { - gl.glDisable(GL.GL_POLYGON_OFFSET_FILL); - context.polyOffsetEnabled = false; - context.polyOffsetFactor = 0; - context.polyOffsetUnits = 0; - } - } - - if (state.getFaceCullMode() != context.cullMode) { - if (state.getFaceCullMode() == RenderState.FaceCullMode.Off) { - gl.glDisable(GL.GL_CULL_FACE); - } else { - gl.glEnable(GL.GL_CULL_FACE); - } - - switch (state.getFaceCullMode()) { - case Off: - break; - case Back: - gl.glCullFace(GL.GL_BACK); - break; - case Front: - gl.glCullFace(GL.GL_FRONT); - break; - case FrontAndBack: - gl.glCullFace(GL.GL_FRONT_AND_BACK); - break; - default: - throw new UnsupportedOperationException("Unrecognized face cull mode: " - + state.getFaceCullMode()); - } - - context.cullMode = state.getFaceCullMode(); - } - - if (state.getBlendMode() != context.blendMode) { - if (state.getBlendMode() == RenderState.BlendMode.Off) { - gl.glDisable(GL.GL_BLEND); - } else { - gl.glEnable(GL.GL_BLEND); - switch (state.getBlendMode()) { - case Off: - break; - case Additive: - gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE); - break; - case AlphaAdditive: - gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE); - break; - case Color: - gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_COLOR); - break; - case Alpha: - gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA); - break; - case PremultAlpha: - gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA); - break; - case Modulate: - gl.glBlendFunc(GL.GL_DST_COLOR, GL.GL_ZERO); - break; - case ModulateX2: - gl.glBlendFunc(GL.GL_DST_COLOR, GL.GL_SRC_COLOR); - break; - case Screen: - gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_COLOR); - break; - case Exclusion: - gl.glBlendFunc(GL.GL_ONE_MINUS_DST_COLOR, GL.GL_ONE_MINUS_SRC_COLOR); - break; - default: - throw new UnsupportedOperationException("Unrecognized blend mode: " - + state.getBlendMode()); - } - } - - context.blendMode = state.getBlendMode(); - } - - if (context.stencilTest != state.isStencilTest() - || context.frontStencilStencilFailOperation != state.getFrontStencilStencilFailOperation() - || context.frontStencilDepthFailOperation != state.getFrontStencilDepthFailOperation() - || context.frontStencilDepthPassOperation != state.getFrontStencilDepthPassOperation() - || context.backStencilStencilFailOperation != state.getBackStencilStencilFailOperation() - || context.backStencilDepthFailOperation != state.getBackStencilDepthFailOperation() - || context.backStencilDepthPassOperation != state.getBackStencilDepthPassOperation() - || context.frontStencilFunction != state.getFrontStencilFunction() - || context.backStencilFunction != state.getBackStencilFunction()) { - - context.frontStencilStencilFailOperation = state.getFrontStencilStencilFailOperation(); //terrible looking, I know - context.frontStencilDepthFailOperation = state.getFrontStencilDepthFailOperation(); - context.frontStencilDepthPassOperation = state.getFrontStencilDepthPassOperation(); - context.backStencilStencilFailOperation = state.getBackStencilStencilFailOperation(); - context.backStencilDepthFailOperation = state.getBackStencilDepthFailOperation(); - context.backStencilDepthPassOperation = state.getBackStencilDepthPassOperation(); - context.frontStencilFunction = state.getFrontStencilFunction(); - context.backStencilFunction = state.getBackStencilFunction(); - - if (state.isStencilTest()) { - gl.glEnable(GL.GL_STENCIL_TEST); - gl.getGL2ES2().glStencilOpSeparate(GL.GL_FRONT, - convertStencilOperation(state.getFrontStencilStencilFailOperation()), - convertStencilOperation(state.getFrontStencilDepthFailOperation()), - convertStencilOperation(state.getFrontStencilDepthPassOperation())); - gl.getGL2ES2().glStencilOpSeparate(GL.GL_BACK, - convertStencilOperation(state.getBackStencilStencilFailOperation()), - convertStencilOperation(state.getBackStencilDepthFailOperation()), - convertStencilOperation(state.getBackStencilDepthPassOperation())); - gl.getGL2ES2().glStencilFuncSeparate(GL.GL_FRONT, - convertTestFunction(state.getFrontStencilFunction()), - 0, Integer.MAX_VALUE); - gl.getGL2ES2().glStencilFuncSeparate(GL.GL_BACK, - convertTestFunction(state.getBackStencilFunction()), - 0, Integer.MAX_VALUE); - } else { - gl.glDisable(GL.GL_STENCIL_TEST); - } - } - } - - private int convertStencilOperation(RenderState.StencilOperation stencilOp) { - switch (stencilOp) { - case Keep: - return GL.GL_KEEP; - case Zero: - return GL.GL_ZERO; - case Replace: - return GL.GL_REPLACE; - case Increment: - return GL.GL_INCR; - case IncrementWrap: - return GL.GL_INCR_WRAP; - case Decrement: - return GL.GL_DECR; - case DecrementWrap: - return GL.GL_DECR_WRAP; - case Invert: - return GL.GL_INVERT; - default: - throw new UnsupportedOperationException("Unrecognized stencil operation: " + stencilOp); - } - } - - private int convertTestFunction(RenderState.TestFunction testFunc) { - switch (testFunc) { - case Never: - return GL.GL_NEVER; - case Less: - return GL.GL_LESS; - case LessOrEqual: - return GL.GL_LEQUAL; - case Greater: - return GL.GL_GREATER; - case GreaterOrEqual: - return GL.GL_GEQUAL; - case Equal: - return GL.GL_EQUAL; - case NotEqual: - return GL.GL_NOTEQUAL; - case Always: - return GL.GL_ALWAYS; - default: - throw new UnsupportedOperationException("Unrecognized test function: " + testFunc); - } - } - - /*********************************************************************\ - |* Camera and World transforms *| - \*********************************************************************/ - @Override - public void setViewPort(int x, int y, int w, int h) { - if (x != vpX || vpY != y || vpW != w || vpH != h) { - GL gl = GLContext.getCurrentGL(); - gl.glViewport(x, y, w, h); - vpX = x; - vpY = y; - vpW = w; - vpH = h; - } - } - - @Override - public void setClipRect(int x, int y, int width, int height) { - GL gl = GLContext.getCurrentGL(); - if (!context.clipRectEnabled) { - gl.glEnable(GL.GL_SCISSOR_TEST); - context.clipRectEnabled = true; - } - if (clipX != x || clipY != y || clipW != width || clipH != height) { - gl.glScissor(x, y, width, height); - clipX = x; - clipY = y; - clipW = width; - clipH = height; - } - } - - @Override - public void clearClipRect() { - if (context.clipRectEnabled) { - GL gl = GLContext.getCurrentGL(); - gl.glDisable(GL.GL_SCISSOR_TEST); - context.clipRectEnabled = false; - - clipX = 0; - clipY = 0; - clipW = 0; - clipH = 0; - } - } - - @Override - public void postFrame() { - objManager.deleteUnused(this); - } - - /*********************************************************************\ - |* Shaders *| - \*********************************************************************/ - protected void updateUniformLocation(Shader shader, Uniform uniform) { - GL gl = GLContext.getCurrentGL(); - // passing a null terminated string is not necessary with JOGL 2.0 - int loc = gl.getGL2ES2().glGetUniformLocation(shader.getId(), uniform.getName()); - if (loc < 0) { - uniform.setLocation(-1); - // uniform is not declared in shader - logger.log(Level.FINE, "Uniform {0} is not declared in shader {1}.", new Object[]{uniform.getName(), shader.getSources()}); - - } else { - uniform.setLocation(loc); - } - } - - protected void bindProgram(Shader shader) { - int shaderId = shader.getId(); - if (context.boundShaderProgram != shaderId) { - GL gl = GLContext.getCurrentGL(); - gl.getGL2ES2().glUseProgram(shaderId); - statistics.onShaderUse(shader, true); - boundShader = shader; - context.boundShaderProgram = shaderId; - } else { - statistics.onShaderUse(shader, false); - } - } - - protected void updateUniform(Shader shader, Uniform uniform) { - int shaderId = shader.getId(); - - assert uniform.getName() != null; - assert shaderId > 0; - - bindProgram(shader); - - int loc = uniform.getLocation(); - if (loc == -1) { - return; - } - - if (loc == -2) { - // get uniform location - updateUniformLocation(shader, uniform); - if (uniform.getLocation() == -1) { - // not declared, ignore - uniform.clearUpdateNeeded(); - return; - } - loc = uniform.getLocation(); - } - - if (uniform.getVarType() == null) { - return; // value not set yet.. - } - statistics.onUniformSet(); - - uniform.clearUpdateNeeded(); - FloatBuffer fb; - IntBuffer ib; - GL gl = GLContext.getCurrentGL(); - switch (uniform.getVarType()) { - case Float: - Float f = (Float) uniform.getValue(); - gl.getGL2ES2().glUniform1f(loc, f.floatValue()); - break; - case Vector2: - Vector2f v2 = (Vector2f) uniform.getValue(); - gl.getGL2ES2().glUniform2f(loc, v2.getX(), v2.getY()); - break; - case Vector3: - Vector3f v3 = (Vector3f) uniform.getValue(); - gl.getGL2ES2().glUniform3f(loc, v3.getX(), v3.getY(), v3.getZ()); - break; - case Vector4: - Object val = uniform.getValue(); - if (val instanceof ColorRGBA) { - ColorRGBA c = (ColorRGBA) val; - gl.getGL2ES2().glUniform4f(loc, c.r, c.g, c.b, c.a); - } else if (val instanceof Vector4f) { - Vector4f c = (Vector4f) val; - gl.getGL2ES2().glUniform4f(loc, c.x, c.y, c.z, c.w); - } else { - Quaternion c = (Quaternion) uniform.getValue(); - gl.getGL2ES2().glUniform4f(loc, c.getX(), c.getY(), c.getZ(), c.getW()); - } - break; - case Boolean: - Boolean b = (Boolean) uniform.getValue(); - gl.getGL2ES2().glUniform1i(loc, b.booleanValue() ? GL.GL_TRUE : GL.GL_FALSE); - break; - case Matrix3: - fb = (FloatBuffer) uniform.getValue(); - assert fb.remaining() == 9; - gl.getGL2ES2().glUniformMatrix3fv(loc, 1, false, fb); - break; - case Matrix4: - fb = (FloatBuffer) uniform.getValue(); - assert fb.remaining() == 16; - gl.getGL2ES2().glUniformMatrix4fv(loc, 1, false, fb); - break; - case IntArray: - ib = (IntBuffer) uniform.getValue(); - gl.getGL2ES2().glUniform1iv(loc, ib.remaining(), ib); - break; - case FloatArray: - fb = (FloatBuffer) uniform.getValue(); - gl.getGL2ES2().glUniform1fv(loc, fb.remaining(), fb); - break; - case Vector2Array: - fb = (FloatBuffer) uniform.getValue(); - gl.getGL2ES2().glUniform2fv(loc, fb.remaining(), fb); - break; - case Vector3Array: - fb = (FloatBuffer) uniform.getValue(); - gl.getGL2ES2().glUniform3fv(loc, fb.remaining(), fb); - break; - case Vector4Array: - fb = (FloatBuffer) uniform.getValue(); - gl.getGL2ES2().glUniform4fv(loc, fb.remaining(), fb); - break; - case Matrix4Array: - fb = (FloatBuffer) uniform.getValue(); - gl.getGL2ES2().glUniformMatrix4fv(loc, 1, false, fb); - break; - case Int: - Integer i = (Integer) uniform.getValue(); - gl.getGL2ES2().glUniform1i(loc, i.intValue()); - break; - default: - throw new UnsupportedOperationException("Unsupported uniform type: " + uniform.getVarType()); - } - } - - protected void updateShaderUniforms(Shader shader) { - ListMap uniforms = shader.getUniformMap(); - for (int i = 0; i < uniforms.size(); i++) { - Uniform uniform = uniforms.getValue(i); - if (uniform.isUpdateNeeded()) { - updateUniform(shader, uniform); - } - } - } - - protected void resetUniformLocations(Shader shader) { - ListMap uniforms = shader.getUniformMap(); - for (int i = 0; i < uniforms.size(); i++) { - Uniform uniform = uniforms.getValue(i); - uniform.reset(); // e.g check location again - } - } - - public int convertShaderType(Shader.ShaderType type) { - switch (type) { - case Fragment: - return GL2ES2.GL_FRAGMENT_SHADER; - case Vertex: - return GL2ES2.GL_VERTEX_SHADER; -// case Geometry: -// return GL3.GL_GEOMETRY_SHADER_ARB; - default: - throw new UnsupportedOperationException("Unrecognized shader type."); - } - } - - public void updateShaderSourceData(ShaderSource source) { - int id = source.getId(); - GL gl = GLContext.getCurrentGL(); - if (id == -1) { - // Create id -// if (gl.isGL2ES2()) { - id = gl.getGL2ES2().glCreateShader(convertShaderType(source.getType())); -// } -// else { -// if (gl.isGL2()) { -// id = gl.getGL2().glCreateShaderObjectARB(convertShaderType(source.getType())); -// } -// } - if (id <= 0) { - throw new RendererException("Invalid ID received when trying to create shader."); - } - - source.setId(id); - } else { - throw new RendererException("Cannot recompile shader source"); - } - - // Upload shader source. - // Merge the defines and source code. - String language = source.getLanguage(); - stringBuf.setLength(0); - if (language.startsWith("GLSL")) { - int version = Integer.parseInt(language.substring(4)); - if (version > 100) { - stringBuf.append("#version "); - stringBuf.append(language.substring(4)); - if (version >= 150) { - stringBuf.append(" core"); - } - stringBuf.append("\n"); - } - } - updateNameBuffer(); - - byte[] definesCodeData = source.getDefines().getBytes(); - byte[] sourceCodeData = source.getSource().getBytes(); - ByteBuffer codeBuf = BufferUtils.createByteBuffer(nameBuf.limit() - + definesCodeData.length - + sourceCodeData.length); - codeBuf.put(nameBuf); - codeBuf.put(definesCodeData); - codeBuf.put(sourceCodeData); - codeBuf.flip(); - - byte[] array = new byte[codeBuf.limit()]; - codeBuf.rewind(); - codeBuf.get(array); - codeBuf.rewind(); - - gl.getGL2ES2().glShaderSource(id, 1, new String[]{new String(array)}, new int[]{array.length}, 0); - gl.getGL2ES2().glCompileShader(id); - - gl.getGL2ES2().glGetShaderiv(id, GL2ES2.GL_COMPILE_STATUS, intBuf1); - - boolean compiledOK = intBuf1.get(0) == GL.GL_TRUE; - String infoLog = null; - - if (VALIDATE_SHADER || !compiledOK) { - // even if compile succeeded, check - // log for warnings - gl.getGL2ES2().glGetShaderiv(id, GL2ES2.GL_INFO_LOG_LENGTH, intBuf1); - int length = intBuf1.get(0); - if (length > 3) { - // get infos - ByteBuffer logBuf = BufferUtils.createByteBuffer(length); - gl.getGL2ES2().glGetShaderInfoLog(id, length, null, logBuf); - byte[] logBytes = new byte[length]; - logBuf.get(logBytes, 0, length); - // convert to string, etc - infoLog = new String(logBytes); - } - } - - if (compiledOK) { - if (infoLog != null) { - logger.log(Level.FINE, "{0} compile success\n{1}", - new Object[]{source.getName(), infoLog}); - } else { - logger.log(Level.FINE, "{0} compile success", source.getName()); - } - source.clearUpdateNeeded(); - } else { - logger.log(Level.WARNING, "Bad compile of:\n{0}", - new Object[]{ShaderDebug.formatShaderSource(stringBuf.toString() + source.getDefines() + source.getSource())}); - if (infoLog != null) { - throw new RendererException("compile error in: " + source + "\n" + infoLog); - } else { - throw new RendererException("compile error in: " + source + "\nerror: "); - } - } - } - - public void updateShaderData(Shader shader) { - GL gl = GLContext.getCurrentGL(); - int id = shader.getId(); - boolean needRegister = false; - if (id == -1) { - // create program - id = gl.getGL2ES2().glCreateProgram(); - if (id == 0) { - throw new RendererException("Invalid ID (" + id + ") received when trying to create shader program."); - } - - shader.setId(id); - needRegister = true; - } - - for (ShaderSource source : shader.getSources()) { - if (source.isUpdateNeeded()) { - updateShaderSourceData(source); - } - gl.getGL2ES2().glAttachShader(id, source.getId()); - } - - if (gl.isGL2GL3() && gl.isExtensionAvailable("GL_EXT_gpu_shader4")) { - // Check if GLSL version is 1.5 for shader - gl.getGL2GL3().glBindFragDataLocation(id, 0, "outFragColor"); - // For MRT - for (int i = 0; i < maxMRTFBOAttachs; i++) { - gl.getGL2GL3().glBindFragDataLocation(id, i, "outFragData[" + i + "]"); - } - } - - // Link shaders to program - gl.getGL2ES2().glLinkProgram(id); - - // Check link status - gl.getGL2ES2().glGetProgramiv(id, GL2ES2.GL_LINK_STATUS, intBuf1); - boolean linkOK = intBuf1.get(0) == GL.GL_TRUE; - String infoLog = null; - - if (VALIDATE_SHADER || !linkOK) { - gl.getGL2ES2().glGetProgramiv(id, GL2ES2.GL_INFO_LOG_LENGTH, intBuf1); - int length = intBuf1.get(0); - if (length > 3) { - // get infos - ByteBuffer logBuf = BufferUtils.createByteBuffer(length); - gl.getGL2ES2().glGetProgramInfoLog(id, length, null, logBuf); - - // convert to string, etc - byte[] logBytes = new byte[length]; - logBuf.get(logBytes, 0, length); - infoLog = new String(logBytes); - } - } - - if (linkOK) { - if (infoLog != null) { - logger.log(Level.FINE, "shader link success. \n{0}", infoLog); - } else { - logger.fine("shader link success"); - } - shader.clearUpdateNeeded(); - if (needRegister) { - // Register shader for clean up if it was created in this method. - objManager.registerObject(shader); - statistics.onNewShader(); - } else { - // OpenGL spec: uniform locations may change after re-link - resetUniformLocations(shader); - } - } else { - if (infoLog != null) { - throw new RendererException("Shader failed to link, shader:" + shader + "\n" + infoLog); - } else { - throw new RendererException("Shader failed to link, shader:" + shader + "\ninfo: "); - } - } - } - - @Override - public void setShader(Shader shader) { - if (shader == null) { - throw new IllegalArgumentException("Shader cannot be null"); - } else { - if (shader.isUpdateNeeded()) { - updateShaderData(shader); - } - - // NOTE: might want to check if any of the - // sources need an update? - - assert shader.getId() > 0; - - updateShaderUniforms(shader); - bindProgram(shader); - } - } - - @Override - public void deleteShaderSource(ShaderSource source) { - if (source.getId() < 0) { - logger.warning("Shader source is not uploaded to GPU, cannot delete."); - return; - } - source.clearUpdateNeeded(); - GL gl = GLContext.getCurrentGL(); - gl.getGL2ES2().glDeleteShader(source.getId()); - source.resetObject(); - } - - @Override - public void deleteShader(Shader shader) { - if (shader.getId() == -1) { - logger.warning("Shader is not uploaded to GPU, cannot delete."); - return; - } - - GL gl = GLContext.getCurrentGL(); - for (ShaderSource source : shader.getSources()) { - if (source.getId() != -1) { - gl.getGL2ES2().glDetachShader(shader.getId(), source.getId()); - deleteShaderSource(source); - } - } - - gl.getGL2ES2().glDeleteProgram(shader.getId()); - statistics.onDeleteShader(); - shader.resetObject(); - } - - /*********************************************************************\ - |* Framebuffers *| - \*********************************************************************/ - @Override - public void copyFrameBuffer(FrameBuffer src, FrameBuffer dst, boolean copyDepth) { - GL gl = GLContext.getCurrentGL(); - if (gl.isExtensionAvailable("GL_EXT_framebuffer_blit") && gl.isGL2GL3()) { - int srcX0 = 0; - int srcY0 = 0; - int srcX1; - int srcY1; - - int dstX0 = 0; - int dstY0 = 0; - int dstX1; - int dstY1; - - int prevFBO = context.boundFBO; - - if (mainFbOverride != null) { - if (src == null) { - src = mainFbOverride; - } - if (dst == null) { - dst = mainFbOverride; - } - } - - if (src != null && src.isUpdateNeeded()) { - updateFrameBuffer(src); - } - - if (dst != null && dst.isUpdateNeeded()) { - updateFrameBuffer(dst); - } - - if (src == null) { - gl.glBindFramebuffer(GL2GL3.GL_READ_FRAMEBUFFER, 0); - srcX0 = vpX; - srcY0 = vpY; - srcX1 = vpX + vpW; - srcY1 = vpY + vpH; - } else { - gl.glBindFramebuffer(GL2GL3.GL_READ_FRAMEBUFFER, src.getId()); - srcX1 = src.getWidth(); - srcY1 = src.getHeight(); - } - if (dst == null) { - gl.glBindFramebuffer(GL2GL3.GL_DRAW_FRAMEBUFFER, 0); - dstX0 = vpX; - dstY0 = vpY; - dstX1 = vpX + vpW; - dstY1 = vpY + vpH; - } else { - gl.glBindFramebuffer(GL2GL3.GL_DRAW_FRAMEBUFFER, dst.getId()); - dstX1 = dst.getWidth(); - dstY1 = dst.getHeight(); - } - int mask = GL.GL_COLOR_BUFFER_BIT; - if (copyDepth) { - mask |= GL.GL_DEPTH_BUFFER_BIT; - } - gl.getGL2GL3().glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, - dstX0, dstY0, dstX1, dstY1, mask, - GL.GL_NEAREST); - gl.glBindFramebuffer(GL2GL3.GL_FRAMEBUFFER, prevFBO); - - - try { - checkFrameBufferError(); - } catch (IllegalStateException ex) { - logger.log(Level.SEVERE, "Source FBO:\n{0}", src); - logger.log(Level.SEVERE, "Dest FBO:\n{0}", dst); - throw ex; - } - } else { - throw new RendererException("EXT_framebuffer_blit required."); - // TODO: support non-blit copies? - } - } - - private String getTargetBufferName(int buffer) { - switch (buffer) { - case GL.GL_NONE: - return "NONE"; - case GL.GL_FRONT: - return "GL_FRONT"; - case GL.GL_BACK: - return "GL_BACK"; - default: - if (buffer >= GL.GL_COLOR_ATTACHMENT0 - && buffer <= GL2ES2.GL_COLOR_ATTACHMENT15) { - return "GL_COLOR_ATTACHMENT" - + (buffer - GL.GL_COLOR_ATTACHMENT0); - } else { - return "UNKNOWN? " + buffer; - } - } - } - - private void printRealRenderBufferInfo(FrameBuffer fb, RenderBuffer rb, String name) { - GL gl = GLContext.getCurrentGL(); - System.out.println("== Renderbuffer " + name + " =="); - System.out.println("RB ID: " + rb.getId()); - System.out.println("Is proper? " + gl.glIsRenderbuffer(rb.getId())); - - int attachment = convertAttachmentSlot(rb.getSlot()); - - gl.glGetFramebufferAttachmentParameteriv(GL2GL3.GL_DRAW_FRAMEBUFFER, - attachment, - GL.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, intBuf16); - int type = intBuf16.get(0); - gl.glGetFramebufferAttachmentParameteriv(GL2GL3.GL_DRAW_FRAMEBUFFER, - attachment, - GL.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, intBuf16); - int rbName = intBuf16.get(0); - - switch (type) { - case GL.GL_NONE: - System.out.println("Type: None"); - break; - case GL.GL_TEXTURE: - System.out.println("Type: Texture"); - break; - case GL.GL_RENDERBUFFER: - System.out.println("Type: Buffer"); - System.out.println("RB ID: " + rbName); - break; - } - - - - } - - private void printRealFrameBufferInfo(FrameBuffer fb) { - GL gl = GLContext.getCurrentGL(); - final byte[] param = new byte[1]; - gl.glGetBooleanv(GL2GL3.GL_DOUBLEBUFFER, param, 0); - boolean doubleBuffer = param[0] != (byte) 0x00; - gl.glGetIntegerv(GL2GL3.GL_DRAW_BUFFER, intBuf16); - String drawBuf = getTargetBufferName(intBuf16.get(0)); - gl.glGetIntegerv(GL2GL3.GL_READ_BUFFER, intBuf16); - String readBuf = getTargetBufferName(intBuf16.get(0)); - - int fbId = fb.getId(); - gl.glGetIntegerv(GL2GL3.GL_DRAW_FRAMEBUFFER_BINDING, intBuf16); - int curDrawBinding = intBuf16.get(0); - gl.glGetIntegerv(GL2GL3.GL_READ_FRAMEBUFFER_BINDING, intBuf16); - int curReadBinding = intBuf16.get(0); - - System.out.println("=== OpenGL FBO State ==="); - System.out.println("Context doublebuffered? " + doubleBuffer); - System.out.println("FBO ID: " + fbId); - System.out.println("Is proper? " + gl.glIsFramebuffer(fbId)); - System.out.println("Is bound to draw? " + (fbId == curDrawBinding)); - System.out.println("Is bound to read? " + (fbId == curReadBinding)); - System.out.println("Draw buffer: " + drawBuf); - System.out.println("Read buffer: " + readBuf); - - if (context.boundFBO != fbId) { - gl.glBindFramebuffer(GL2GL3.GL_DRAW_FRAMEBUFFER, fbId); - context.boundFBO = fbId; - } - - if (fb.getDepthBuffer() != null) { - printRealRenderBufferInfo(fb, fb.getDepthBuffer(), "Depth"); - } - for (int i = 0; i < fb.getNumColorBuffers(); i++) { - printRealRenderBufferInfo(fb, fb.getColorBuffer(i), "Color" + i); - } - } - - private void checkFrameBufferError() { - GL gl = GLContext.getCurrentGL(); - int status = gl.glCheckFramebufferStatus(GL.GL_FRAMEBUFFER); - switch (status) { - case GL.GL_FRAMEBUFFER_COMPLETE: - break; - case GL.GL_FRAMEBUFFER_UNSUPPORTED: - // Choose different formats - throw new IllegalStateException("Framebuffer object format is " - + "unsupported by the video hardware."); - case GL.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: - throw new IllegalStateException("Framebuffer has erronous attachment."); - case GL.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: - throw new IllegalStateException("Framebuffer is missing required attachment."); - case GL.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: - throw new IllegalStateException( - "Framebuffer attachments must have same dimensions."); - case GL.GL_FRAMEBUFFER_INCOMPLETE_FORMATS: - throw new IllegalStateException("Framebuffer attachments must have same formats."); - case GL2GL3.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: - throw new IllegalStateException("Incomplete draw buffer."); - case GL2GL3.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: - throw new IllegalStateException("Incomplete read buffer."); - case GL2GL3.GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: - throw new IllegalStateException("Incomplete multisample buffer."); - default: - // Programming error; will fail on all hardware - throw new IllegalStateException("Some video driver error " - + "or programming error occured. " - + "Framebuffer object status is invalid. "); - } - } - - private void updateRenderBuffer(FrameBuffer fb, RenderBuffer rb) { - GL gl = GLContext.getCurrentGL(); - int id = rb.getId(); - if (id == -1) { - gl.glGenRenderbuffers(1, intBuf1); - id = intBuf1.get(0); - rb.setId(id); - } - - if (context.boundRB != id) { - gl.glBindRenderbuffer(GL.GL_RENDERBUFFER, id); - context.boundRB = id; - } - - if (fb.getWidth() > maxRBSize || fb.getHeight() > maxRBSize) { - throw new RendererException("Resolution " + fb.getWidth() - + ":" + fb.getHeight() + " is not supported."); - } - - TextureUtil.GLImageFormat glFmt = TextureUtil.getImageFormatWithError(rb.getFormat(), fb.isSrgb()); - - if (fb.getSamples() > 1 && gl.isExtensionAvailable("GL_EXT_framebuffer_multisample") - && gl.isGL2GL3()/*&& gl.isFunctionAvailable("glRenderbufferStorageMultisample")*/) { - int samples = fb.getSamples(); - if (maxFBOSamples < samples) { - samples = maxFBOSamples; - } - gl.getGL2GL3() - .glRenderbufferStorageMultisample(GL.GL_RENDERBUFFER, samples, - glFmt.internalFormat, fb.getWidth(), - fb.getHeight()); - } else { - gl.glRenderbufferStorage(GL.GL_RENDERBUFFER, - glFmt.internalFormat, fb.getWidth(), fb.getHeight()); - } - } - - private int convertAttachmentSlot(int attachmentSlot) { - // can also add support for stencil here - if (attachmentSlot == FrameBuffer.SLOT_DEPTH) { - return GL.GL_DEPTH_ATTACHMENT; - } else if (attachmentSlot == FrameBuffer.SLOT_DEPTH_STENCIL) { - return GL2ES3.GL_DEPTH_STENCIL_ATTACHMENT; - } else if (attachmentSlot < 0 || attachmentSlot >= 16) { - throw new UnsupportedOperationException("Invalid FBO attachment slot: " + attachmentSlot); - } - - return GL.GL_COLOR_ATTACHMENT0 + attachmentSlot; - } - - public void updateRenderTexture(FrameBuffer fb, RenderBuffer rb) { - GL gl = GLContext.getCurrentGL(); - Texture tex = rb.getTexture(); - Image image = tex.getImage(); - if (image.isUpdateNeeded()) { - updateTexImageData(image, tex.getType(), 0); - - // NOTE: For depth textures, sets nearest/no-mips mode - // Required to fix "framebuffer unsupported" - // for old NVIDIA drivers! - setupTextureParams(tex); - } - - gl.glFramebufferTexture2D(GL.GL_FRAMEBUFFER, - convertAttachmentSlot(rb.getSlot()), - convertTextureType(tex.getType(), image.getMultiSamples(), rb.getFace()), - image.getId(), - 0); - } - - public void updateFrameBufferAttachment(FrameBuffer fb, RenderBuffer rb) { - boolean needAttach; - if (rb.getTexture() == null) { - // if it hasn't been created yet, then attach is required. - needAttach = rb.getId() == -1; - updateRenderBuffer(fb, rb); - } else { - needAttach = false; - updateRenderTexture(fb, rb); - } - if (needAttach) { - GL gl = GLContext.getCurrentGL(); - gl.glFramebufferRenderbuffer(GL.GL_FRAMEBUFFER, convertAttachmentSlot(rb.getSlot()), - GL.GL_RENDERBUFFER, rb.getId()); - } - } - - public void updateFrameBuffer(FrameBuffer fb) { - GL gl = GLContext.getCurrentGL(); - int id = fb.getId(); - if (id == -1) { - // create FBO - gl.glGenFramebuffers(1, intBuf1); - id = intBuf1.get(0); - fb.setId(id); - objManager.registerObject(fb); - - statistics.onNewFrameBuffer(); - } - - if (context.boundFBO != id) { - gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, id); - // binding an FBO automatically sets draw buf to GL_COLOR_ATTACHMENT0 - context.boundDrawBuf = 0; - context.boundFBO = id; - } - - FrameBuffer.RenderBuffer depthBuf = fb.getDepthBuffer(); - if (depthBuf != null) { - updateFrameBufferAttachment(fb, depthBuf); - } - - for (int i = 0; i < fb.getNumColorBuffers(); i++) { - FrameBuffer.RenderBuffer colorBuf = fb.getColorBuffer(i); - updateFrameBufferAttachment(fb, colorBuf); - } - - fb.clearUpdateNeeded(); - } - - public Vector2f[] getFrameBufferSamplePositions(FrameBuffer fb) { - if (fb.getSamples() <= 1) { - throw new IllegalArgumentException("Framebuffer must be multisampled"); - } - - setFrameBuffer(fb); - - Vector2f[] samplePositions = new Vector2f[fb.getSamples()]; - FloatBuffer samplePos = BufferUtils.createFloatBuffer(2); - GL gl = GLContext.getCurrentGL(); - if (gl.isGL2GL3()) { - for (int i = 0; i < samplePositions.length; i++) { - gl.getGL3().glGetMultisamplefv(GL3.GL_SAMPLE_POSITION, i, samplePos); - samplePos.clear(); - samplePositions[i] = new Vector2f(samplePos.get(0) - 0.5f, - samplePos.get(1) - 0.5f); - } - } - return samplePositions; - } - - @Override - public void setMainFrameBufferOverride(FrameBuffer fb) { - mainFbOverride = fb; - } - - @Override - public void setFrameBuffer(FrameBuffer fb) { - GL gl = GLContext.getCurrentGL(); - if (!gl.isExtensionAvailable("GL_EXT_framebuffer_object")) { - throw new RendererException("Framebuffer objects are not supported" + - " by the video hardware"); - } - - if (fb == null && mainFbOverride != null) { - fb = mainFbOverride; - } - - if (lastFb == fb) { - if (fb == null || !fb.isUpdateNeeded()) { - return; - } - } - - // generate mipmaps for last FB if needed - if (lastFb != null) { - for (int i = 0; i < lastFb.getNumColorBuffers(); i++) { - RenderBuffer rb = lastFb.getColorBuffer(i); - Texture tex = rb.getTexture(); - if (tex != null - && tex.getMinFilter().usesMipMapLevels()) { - setTexture(0, rb.getTexture()); - - int textureType = convertTextureType(tex.getType(), tex.getImage().getMultiSamples(), rb.getFace()); - gl.glEnable(textureType); - gl.glGenerateMipmap(textureType); - gl.glDisable(textureType); - } - } - } - - if (fb == null) { - // unbind any fbos - if (context.boundFBO != 0) { - gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0); - statistics.onFrameBufferUse(null, true); - - context.boundFBO = 0; - } - // select back buffer - if (context.boundDrawBuf != -1) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glDrawBuffer(initialDrawBuf); - } - context.boundDrawBuf = -1; - } - if (context.boundReadBuf != -1) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glReadBuffer(initialReadBuf); - } - context.boundReadBuf = -1; - } - - lastFb = null; - } else { - if (fb.getNumColorBuffers() == 0 && fb.getDepthBuffer() == null) { - throw new IllegalArgumentException("The framebuffer: " + fb - + "\nDoesn't have any color/depth buffers"); - } - - if (fb.isUpdateNeeded()) { - updateFrameBuffer(fb); - } - - if (context.boundFBO != fb.getId()) { - gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, fb.getId()); - statistics.onFrameBufferUse(fb, true); - - // update viewport to reflect framebuffer's resolution - setViewPort(0, 0, fb.getWidth(), fb.getHeight()); - - context.boundFBO = fb.getId(); - } else { - statistics.onFrameBufferUse(fb, false); - } - if (fb.getNumColorBuffers() == 0) { - // make sure to select NONE as draw buf - // no color buffer attached. select NONE - if (context.boundDrawBuf != -2) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glDrawBuffer(GL.GL_NONE); - } - context.boundDrawBuf = -2; - } - if (context.boundReadBuf != -2) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glReadBuffer(GL.GL_NONE); - } - context.boundReadBuf = -2; - } - } else { - if (fb.getNumColorBuffers() > maxFBOAttachs) { - throw new RendererException("Framebuffer has more color " - + "attachments than are supported" - + " by the video hardware!"); - } - if (fb.isMultiTarget()) { - if (fb.getNumColorBuffers() > maxMRTFBOAttachs) { - throw new RendererException("Framebuffer has more" - + " multi targets than are supported" - + " by the video hardware!"); - } - - if (context.boundDrawBuf != 100 + fb.getNumColorBuffers()) { - intBuf16.clear(); - for (int i = 0; i < fb.getNumColorBuffers(); i++) { - intBuf16.put(GL.GL_COLOR_ATTACHMENT0 + i); - } - - intBuf16.flip(); - if (gl.isGL2GL3()) { - gl.getGL2GL3().glDrawBuffers(intBuf16.limit(), intBuf16); - } - context.boundDrawBuf = 100 + fb.getNumColorBuffers(); - } - } else { - RenderBuffer rb = fb.getColorBuffer(fb.getTargetIndex()); - // select this draw buffer - if (context.boundDrawBuf != rb.getSlot()) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glDrawBuffer(GL.GL_COLOR_ATTACHMENT0 + rb.getSlot()); - } - context.boundDrawBuf = rb.getSlot(); - } - } - } - - assert fb.getId() >= 0; - assert context.boundFBO == fb.getId(); - lastFb = fb; - - try { - checkFrameBufferError(); - } catch (IllegalStateException ex) { - logger.log(Level.SEVERE, "=== jMonkeyEngine FBO State ===\n{0}", fb); - printRealFrameBufferInfo(fb); - throw ex; - } - } - } - - @Override - public void readFrameBuffer(FrameBuffer fb, ByteBuffer byteBuf) { - GL gl = GLContext.getCurrentGL(); - if (fb != null) { - RenderBuffer rb = fb.getColorBuffer(); - if (rb == null) { - throw new IllegalArgumentException("Specified framebuffer" - + " does not have a colorbuffer"); - } - - setFrameBuffer(fb); - if (context.boundReadBuf != rb.getSlot()) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glReadBuffer(GL.GL_COLOR_ATTACHMENT0 + rb.getSlot()); - } - context.boundReadBuf = rb.getSlot(); - } - } else { - setFrameBuffer(null); - } - - gl.glReadPixels(vpX, vpY, vpW, vpH, /*GL.GL_RGBA*/ GL.GL_BGRA, GL.GL_UNSIGNED_BYTE, byteBuf); - } - - private void deleteRenderBuffer(FrameBuffer fb, RenderBuffer rb) { - intBuf1.put(0, rb.getId()); - GL gl = GLContext.getCurrentGL(); - gl.glDeleteRenderbuffers(1, intBuf1); - } - - @Override - public void deleteFrameBuffer(FrameBuffer fb) { - if (fb.getId() != -1) { - GL gl = GLContext.getCurrentGL(); - if (context.boundFBO == fb.getId()) { - gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0); - context.boundFBO = 0; - } - - if (fb.getDepthBuffer() != null) { - deleteRenderBuffer(fb, fb.getDepthBuffer()); - } - if (fb.getColorBuffer() != null) { - deleteRenderBuffer(fb, fb.getColorBuffer()); - } - - intBuf1.put(0, fb.getId()); - gl.glDeleteFramebuffers(1, intBuf1); - fb.resetObject(); - - statistics.onDeleteFrameBuffer(); - } - } - - /*********************************************************************\ - |* Textures *| - \*********************************************************************/ - private int convertTextureType(Texture.Type type, int samples, int face) { - GL gl = GLContext.getCurrentGL(); - if (samples > 1 && !gl.isExtensionAvailable("GL_ARB_texture_multisample")) { - throw new RendererException("Multisample textures are not supported" + - " by the video hardware."); - } - - switch (type) { - case TwoDimensional: - if (samples > 1) { - return GL3.GL_TEXTURE_2D_MULTISAMPLE; - } else { - return GL.GL_TEXTURE_2D; - } - case TwoDimensionalArray: - if (samples > 1) { - return GL3.GL_TEXTURE_2D_MULTISAMPLE_ARRAY; - } else { - return GL2ES3.GL_TEXTURE_2D_ARRAY; - } - case ThreeDimensional: - return GL2ES2.GL_TEXTURE_3D; - case CubeMap: - if (face < 0) { - return GL.GL_TEXTURE_CUBE_MAP; - } else if (face < 6) { - return GL.GL_TEXTURE_CUBE_MAP_POSITIVE_X + face; - } else { - throw new UnsupportedOperationException("Invalid cube map face index: " + face); - } - default: - throw new UnsupportedOperationException("Unknown texture type: " + type); - } - } - - private int convertMagFilter(Texture.MagFilter filter) { - switch (filter) { - case Bilinear: - return GL.GL_LINEAR; - case Nearest: - return GL.GL_NEAREST; - default: - throw new UnsupportedOperationException("Unknown mag filter: " + filter); - } - } - - private int convertMinFilter(Texture.MinFilter filter) { - switch (filter) { - case Trilinear: - return GL.GL_LINEAR_MIPMAP_LINEAR; - case BilinearNearestMipMap: - return GL.GL_LINEAR_MIPMAP_NEAREST; - case NearestLinearMipMap: - return GL.GL_NEAREST_MIPMAP_LINEAR; - case NearestNearestMipMap: - return GL.GL_NEAREST_MIPMAP_NEAREST; - case BilinearNoMipMaps: - return GL.GL_LINEAR; - case NearestNoMipMaps: - return GL.GL_NEAREST; - default: - throw new UnsupportedOperationException("Unknown min filter: " + filter); - } - } - - private int convertWrapMode(Texture.WrapMode mode) { - switch (mode) { - case BorderClamp: - return GL2GL3.GL_CLAMP_TO_BORDER; - case Clamp: - // Falldown intentional. - case EdgeClamp: - return GL.GL_CLAMP_TO_EDGE; - case Repeat: - return GL.GL_REPEAT; - case MirroredRepeat: - return GL.GL_MIRRORED_REPEAT; - default: - throw new UnsupportedOperationException("Unknown wrap mode: " + mode); - } - } - - @SuppressWarnings("fallthrough") - private void setupTextureParams(Texture tex) { - GL gl = GLContext.getCurrentGL(); - Image image = tex.getImage(); - int target = convertTextureType(tex.getType(), image != null ? image.getMultiSamples() : 1, -1); - - // filter things - int minFilter = convertMinFilter(tex.getMinFilter()); - int magFilter = convertMagFilter(tex.getMagFilter()); - gl.glTexParameteri(target, GL.GL_TEXTURE_MIN_FILTER, minFilter); - gl.glTexParameteri(target, GL.GL_TEXTURE_MAG_FILTER, magFilter); - - if (tex.getAnisotropicFilter() > 1) { - if (gl.isExtensionAvailable("GL_EXT_texture_filter_anisotropic")) { - gl.glTexParameterf(target, - GL.GL_TEXTURE_MAX_ANISOTROPY_EXT, - tex.getAnisotropicFilter()); - } - } - - if (context.pointSprite) { - return; // Attempt to fix glTexParameter crash for some ATI GPUs - } - // repeat modes - switch (tex.getType()) { - case ThreeDimensional: - case CubeMap: // cubemaps use 3D coords - gl.glTexParameteri(target, GL2ES2.GL_TEXTURE_WRAP_R, convertWrapMode(tex.getWrap(WrapAxis.R))); - case TwoDimensional: - case TwoDimensionalArray: - gl.glTexParameteri(target, GL2ES2.GL_TEXTURE_WRAP_T, convertWrapMode(tex.getWrap(WrapAxis.T))); - // fall down here is intentional.. -// case OneDimensional: - gl.glTexParameteri(target, GL2ES2.GL_TEXTURE_WRAP_S, convertWrapMode(tex.getWrap(WrapAxis.S))); - break; - default: - throw new UnsupportedOperationException("Unknown texture type: " + tex.getType()); - } - - if (tex.isNeedCompareModeUpdate()) { - // R to Texture compare mode - if (tex.getShadowCompareMode() != Texture.ShadowCompareMode.Off) { - gl.glTexParameteri(target, GL2ES2.GL_TEXTURE_COMPARE_MODE, GL2.GL_COMPARE_R_TO_TEXTURE); - gl.glTexParameteri(target, GL2.GL_DEPTH_TEXTURE_MODE, GL2.GL_INTENSITY); - if (tex.getShadowCompareMode() == Texture.ShadowCompareMode.GreaterOrEqual) { - gl.glTexParameteri(target, GL2ES2.GL_TEXTURE_COMPARE_FUNC, GL.GL_GEQUAL); - } else { - gl.glTexParameteri(target, GL2ES2.GL_TEXTURE_COMPARE_FUNC, GL.GL_LEQUAL); - } - } else { - //restoring default value - gl.glTexParameteri(target, GL2ES2.GL_TEXTURE_COMPARE_MODE, GL.GL_NONE); - } - tex.compareModeUpdated(); - } - } - - /** - * Uploads the given image to the GL driver. - * - * @param img The image to upload - * @param type How the data in the image argument should be interpreted. - * @param unit The texture slot to be used to upload the image, not important - */ - public void updateTexImageData(Image img, Texture.Type type, int unit) { - int texId = img.getId(); - GL gl = GLContext.getCurrentGL(); - if (texId == -1) { - // create texture - gl.glGenTextures(1, intBuf1); - texId = intBuf1.get(0); - img.setId(texId); - objManager.registerObject(img); - - statistics.onNewTexture(); - } - - // bind texture - int target = convertTextureType(type, img.getMultiSamples(), -1); - if (context.boundTextureUnit != unit) { - gl.glActiveTexture(GL.GL_TEXTURE0 + unit); - context.boundTextureUnit = unit; - } - if (context.boundTextures[unit] != img) { - gl.glBindTexture(target, texId); - context.boundTextures[unit] = img; - - statistics.onTextureUse(img, true); - } - - if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired()) { - // Image does not have mipmaps, but they are required. - // Generate from base level. - - if (!gl.isExtensionAvailable("GL_VERSION_3_0")) { - if (gl.isGL2ES1()) { - gl.glTexParameteri(target, GL2ES1.GL_GENERATE_MIPMAP, GL.GL_TRUE); - img.setMipmapsGenerated(true); - } - } else { - // For OpenGL3 and up. - // We'll generate mipmaps via glGenerateMipmapEXT (see below) - } - } else if (img.hasMipmaps()) { - // Image already has mipmaps, set the max level based on the - // number of mipmaps we have. - if (gl.isGL2GL3()) { - gl.glTexParameteri(target, GL2ES3.GL_TEXTURE_MAX_LEVEL, img.getMipMapSizes().length - 1); - } - } else { - // Image does not have mipmaps and they are not required. - // Specify that that the texture has no mipmaps. - gl.glTexParameteri(target, GL2ES3.GL_TEXTURE_MAX_LEVEL, 0); - } - - int imageSamples = img.getMultiSamples(); - if (imageSamples > 1) { - if (img.getFormat().isDepthFormat()) { - img.setMultiSamples(Math.min(maxDepthTexSamples, imageSamples)); - } else { - img.setMultiSamples(Math.min(maxColorTexSamples, imageSamples)); - } - } - - // Yes, some OpenGL2 cards (GeForce 5) still dont support NPOT. - if (!gl.isExtensionAvailable("GL_ARB_texture_non_power_of_two") && img.isNPOT()) { - if (img.getData(0) == null) { - throw new RendererException("non-power-of-2 framebuffer textures are not supported by the video hardware"); - } else { - MipMapGenerator.resizeToPowerOf2(img); - } - } - - // Check if graphics card doesn't support multisample textures - if (!gl.isExtensionAvailable("GL_ARB_texture_multisample")) { - if (img.getMultiSamples() > 1) { - throw new RendererException("Multisample textures not supported by graphics hardware"); - } - } - - if (target == GL.GL_TEXTURE_CUBE_MAP) { - List data = img.getData(); - if (data.size() != 6) { - logger.log(Level.WARNING, "Invalid texture: {0}\n" - + "Cubemap textures must contain 6 data units.", img); - return; - } - for (int i = 0; i < 6; i++) { - TextureUtil.uploadTexture(img, GL.GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, i, 0, linearizeSrgbImages); - } - } else if (target == GL2ES3.GL_TEXTURE_2D_ARRAY) { - if (!caps.contains(Caps.TextureArray)) { - throw new RendererException("Texture arrays not supported by graphics hardware"); - } - - List data = img.getData(); - - // -1 index specifies prepare data for 2D Array - TextureUtil.uploadTexture(img, target, -1, 0, linearizeSrgbImages); - - for (int i = 0; i < data.size(); i++) { - // upload each slice of 2D array in turn - // this time with the appropriate index - TextureUtil.uploadTexture(img, target, i, 0, linearizeSrgbImages); - } - } else { - TextureUtil.uploadTexture(img, target, 0, 0, linearizeSrgbImages); - } - - if (img.getMultiSamples() != imageSamples) { - img.setMultiSamples(imageSamples); - } - - if (gl.isExtensionAvailable("GL_VERSION_3_0")) { - if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired() && img.getData() != null) { - // XXX: Required for ATI - gl.glEnable(target); - gl.glGenerateMipmap(target); - gl.glDisable(target); - img.setMipmapsGenerated(true); - } - } - - img.clearUpdateNeeded(); - } - - @Override - public void setTexture(int unit, Texture tex) { - GL gl = GLContext.getCurrentGL(); - Image image = tex.getImage(); - if (image.isUpdateNeeded() || (image.isGeneratedMipmapsRequired() && !image.isMipmapsGenerated())) { - updateTexImageData(image, tex.getType(), unit); - } - - int texId = image.getId(); - assert texId != -1; - - Image[] textures = context.boundTextures; - - int type = convertTextureType(tex.getType(), image.getMultiSamples(), -1); -// if (!context.textureIndexList.moveToNew(unit)) { -// if (context.boundTextureUnit != unit){ -// gl.glActiveTexture(GL.GL_TEXTURE0 + unit); -// context.boundTextureUnit = unit; -// } -// gl.glEnable(type); -// } - - if (context.boundTextureUnit != unit) { - gl.glActiveTexture(GL.GL_TEXTURE0 + unit); - context.boundTextureUnit = unit; - } - if (textures[unit] != image) { - gl.glBindTexture(type, texId); - textures[unit] = image; - - statistics.onTextureUse(image, true); - } else { - statistics.onTextureUse(image, false); - } - - setupTextureParams(tex); - } - - @Override - public void modifyTexture(Texture tex, Image pixels, int x, int y) { - setTexture(0, tex); - TextureUtil.uploadSubTexture(pixels, convertTextureType(tex.getType(), tex.getImage().getMultiSamples(), -1), 0, x, y, linearizeSrgbImages); - } - - public void clearTextureUnits() { - /*GL gl = GLContext.getCurrentGL(); - IDList textureList = context.textureIndexList; - Texture[] textures = context.boundTextures; - for (int i = 0; i < textureList.oldLen; i++) { - int idx = textureList.oldList[i]; - - if (context.boundTextureUnit != idx) { - gl.glActiveTexture(GL.GL_TEXTURE0 + idx); - context.boundTextureUnit = idx; - } - gl.glDisable(convertTextureType(textures[idx].getType())); - textures[idx] = null; - } - context.textureIndexList.copyNewToOld();*/ - } - - @Override - public void deleteImage(Image image) { - int texId = image.getId(); - if (texId != -1) { - intBuf1.put(0, texId); - intBuf1.position(0).limit(1); - GL gl = GLContext.getCurrentGL(); - gl.glDeleteTextures(1, intBuf1); - image.resetObject(); - - statistics.onDeleteTexture(); - } - } - - /*********************************************************************\ - |* Vertex Buffers and Attributes *| - \*********************************************************************/ - private int convertUsage(Usage usage) { - switch (usage) { - case Static: - return GL.GL_STATIC_DRAW; - case Dynamic: - return GL.GL_DYNAMIC_DRAW; - case Stream: - return GL2ES2.GL_STREAM_DRAW; - default: - throw new RuntimeException("Unknown usage type: " + usage); - } - } - - private int convertFormat(VertexBuffer.Format format) { - switch (format) { - case Byte: - return GL.GL_BYTE; - case UnsignedByte: - return GL.GL_UNSIGNED_BYTE; - case Short: - return GL.GL_SHORT; - case UnsignedShort: - return GL.GL_UNSIGNED_SHORT; - case Int: - return GL2ES2.GL_INT; - case UnsignedInt: - return GL.GL_UNSIGNED_INT; - case Float: - return GL.GL_FLOAT; - case Double: - return GL2GL3.GL_DOUBLE; - default: - throw new UnsupportedOperationException("Unknown buffer format."); - - } - } - - @Override - public void updateBufferData(VertexBuffer vb) { - GL gl = GLContext.getCurrentGL(); - int bufId = vb.getId(); - boolean created = false; - if (bufId == -1) { - // create buffer - gl.glGenBuffers(1, intBuf1); - bufId = intBuf1.get(0); - vb.setId(bufId); - objManager.registerObject(vb); - - //statistics.onNewVertexBuffer(); - - created = true; - } - - // bind buffer - int target; - if (vb.getBufferType() == VertexBuffer.Type.Index) { - target = GL.GL_ELEMENT_ARRAY_BUFFER; - if (context.boundElementArrayVBO != bufId) { - gl.glBindBuffer(target, bufId); - context.boundElementArrayVBO = bufId; - //statistics.onVertexBufferUse(vb, true); - } else { - //statistics.onVertexBufferUse(vb, false); - } - } else { - target = GL.GL_ARRAY_BUFFER; - if (context.boundArrayVBO != bufId) { - gl.glBindBuffer(target, bufId); - context.boundArrayVBO = bufId; - //statistics.onVertexBufferUse(vb, true); - } else { - //statistics.onVertexBufferUse(vb, false); - } - } - - int usage = convertUsage(vb.getUsage()); - Buffer data = vb.getData(); - data.rewind(); - - if (created || vb.hasDataSizeChanged()) { - // upload data based on format - gl.glBufferData(target, data.capacity() * vb.getFormat().getComponentSize(), data, usage); - } else { - gl.glBufferSubData(target, 0, data.capacity() * vb.getFormat().getComponentSize(), data); - } - - vb.clearUpdateNeeded(); - } - - @Override - public void deleteBuffer(VertexBuffer vb) { - GL gl = GLContext.getCurrentGL(); - int bufId = vb.getId(); - if (bufId != -1) { - // delete buffer - intBuf1.put(0, bufId); - intBuf1.position(0).limit(1); - gl.glDeleteBuffers(1, intBuf1); - vb.resetObject(); - - //statistics.onDeleteVertexBuffer(); - } - } - - public void clearVertexAttribs() { - GL gl = GLContext.getCurrentGL(); - IDList attribList = context.attribIndexList; - for (int i = 0; i < attribList.oldLen; i++) { - int idx = attribList.oldList[i]; - gl.getGL2ES2().glDisableVertexAttribArray(idx); - if (context.boundAttribs[idx].isInstanced() && gl.isGL3ES3()) { - gl.getGL3ES3().glVertexAttribDivisor(idx, 0); - } - context.boundAttribs[idx] = null; - } - context.attribIndexList.copyNewToOld(); - } - - public void setVertexAttrib(VertexBuffer vb, VertexBuffer idb) { - if (vb.getBufferType() == VertexBuffer.Type.Index) { - throw new IllegalArgumentException("Index buffers not allowed to be set to vertex attrib"); - } - - int programId = context.boundShaderProgram; - if (programId > 0) { - GL gl = GLContext.getCurrentGL(); - Attribute attrib = boundShader.getAttribute(vb.getBufferType()); - int loc = attrib.getLocation(); - if (loc == -1) { - return; // not defined - } - if (loc == -2) { - stringBuf.setLength(0); - // JOGL 2.0 doesn't need a null terminated string - stringBuf.append("in").append(vb.getBufferType().name()); - loc = gl.getGL2ES2().glGetAttribLocation(programId, stringBuf.toString()); - - // not really the name of it in the shader (inPosition\0) but - // the internal name of the enum (Position). - if (loc < 0) { - attrib.setLocation(-1); - return; // not available in shader. - } else { - attrib.setLocation(loc); - } - } - - if (vb.isInstanced()) { - if (!gl.isExtensionAvailable("GL_ARB_instanced_arrays") - || !gl.isExtensionAvailable("GL_ARB_draw_instanced")) { - throw new RendererException("Instancing is required, " - + "but not supported by the " - + "graphics hardware"); - } - } - int slotsRequired = 1; - if (vb.getNumComponents() > 4) { - if (vb.getNumComponents() % 4 != 0) { - throw new RendererException("Number of components in multi-slot " - + "buffers must be divisible by 4"); - } - slotsRequired = vb.getNumComponents() / 4; - } - - if (vb.isUpdateNeeded() && idb == null) { - updateBufferData(vb); - } - - VertexBuffer[] attribs = context.boundAttribs; - for (int i = 0; i < slotsRequired; i++) { - if (!context.attribIndexList.moveToNew(loc + i)) { - gl.getGL2ES2().glEnableVertexAttribArray(loc + i); - //System.out.println("Enabled ATTRIB IDX: "+loc + i); - } - } - if (attribs[loc] != vb) { - // NOTE: Use id from interleaved buffer if specified - int bufId = idb != null ? idb.getId() : vb.getId(); - assert bufId != -1; - if (context.boundArrayVBO != bufId) { - gl.glBindBuffer(GL.GL_ARRAY_BUFFER, bufId); - context.boundArrayVBO = bufId; - //statistics.onVertexBufferUse(vb, true); - } else { - //statistics.onVertexBufferUse(vb, false); - } - - if (slotsRequired == 1) { - gl.getGL2ES2().glVertexAttribPointer(loc, - vb.getNumComponents(), - convertFormat(vb.getFormat()), - vb.isNormalized(), - vb.getStride(), - vb.getOffset()); - } else { - for (int i = 0; i < slotsRequired; i++) { - // The pointer maps the next 4 floats in the slot. - // E.g. - // P1: XXXX____________XXXX____________ - // P2: ____XXXX____________XXXX________ - // P3: ________XXXX____________XXXX____ - // P4: ____________XXXX____________XXXX - // stride = 4 bytes in float * 4 floats in slot * num slots - // offset = 4 bytes in float * 4 floats in slot * slot index - gl.getGL2ES2().glVertexAttribPointer(loc + i, - 4, - convertFormat(vb.getFormat()), - vb.isNormalized(), - 4 * 4 * slotsRequired, - 4 * 4 * i); - } - } - - for (int i = 0; i < slotsRequired; i++) { - int slot = loc + i; - if (vb.isInstanced() && (attribs[slot] == null || !attribs[slot].isInstanced())) { - // non-instanced -> instanced - gl.getGL3ES3().glVertexAttribDivisor(slot, vb.getInstanceSpan()); - } else if (!vb.isInstanced() && attribs[slot] != null && attribs[slot].isInstanced()) { - // instanced -> non-instanced - gl.getGL3ES3().glVertexAttribDivisor(slot, 0); - } - attribs[slot] = vb; - } - } - } else { - throw new IllegalStateException("Cannot render mesh without shader bound"); - } - } - - public void setVertexAttrib(VertexBuffer vb) { - setVertexAttrib(vb, null); - } - - public void drawTriangleArray(Mesh.Mode mode, int count, int vertCount) { - boolean useInstancing = count > 1 && caps.contains(Caps.MeshInstancing); - GL gl = GLContext.getCurrentGL(); - if (useInstancing) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glDrawArraysInstanced(convertElementMode(mode), 0, - vertCount, count); - } - } else { - gl.glDrawArrays(convertElementMode(mode), 0, vertCount); - } - } - - public void drawTriangleList(VertexBuffer indexBuf, Mesh mesh, int count) { - if (indexBuf.getBufferType() != VertexBuffer.Type.Index) { - throw new IllegalArgumentException("Only index buffers are allowed as triangle lists."); - } - - if (indexBuf.isUpdateNeeded()) { - updateBufferData(indexBuf); - } - - int bufId = indexBuf.getId(); - assert bufId != -1; - - GL gl = GLContext.getCurrentGL(); - - if (context.boundElementArrayVBO != bufId) { - gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, bufId); - context.boundElementArrayVBO = bufId; - //statistics.onVertexBufferUse(indexBuf, true); - } else { - //statistics.onVertexBufferUse(indexBuf, true); - } - - int vertCount = mesh.getVertexCount(); - boolean useInstancing = count > 1 && caps.contains(Caps.MeshInstancing); - - if (mesh.getMode() == Mode.Hybrid) { - int[] modeStart = mesh.getModeStart(); - int[] elementLengths = mesh.getElementLengths(); - - int elMode = convertElementMode(Mode.Triangles); - int fmt = convertFormat(indexBuf.getFormat()); - int elSize = indexBuf.getFormat().getComponentSize(); - int listStart = modeStart[0]; - int stripStart = modeStart[1]; - int fanStart = modeStart[2]; - int curOffset = 0; - for (int i = 0; i < elementLengths.length; i++) { - if (i == stripStart) { - elMode = convertElementMode(Mode.TriangleStrip); - } else if (i == fanStart) { - elMode = convertElementMode(Mode.TriangleStrip); - } - int elementLength = elementLengths[i]; - - if (useInstancing) { - if (gl.isGL2()) { - indexBuf.getData().position(curOffset); - indexBuf.getData().limit(curOffset + elementLength); - - gl.getGL2().glDrawElementsInstanced(elMode, - elementLength, - fmt, - indexBuf.getData(), - count); - } else { - throw new IllegalArgumentException( - "instancing is not supported."); - } - } else { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glDrawRangeElements(elMode, - 0, - vertCount, - elementLength, - fmt, - curOffset); - } else { - indexBuf.getData().position(curOffset); - gl.getGL2().glDrawElements(elMode, elementLength, fmt, - indexBuf.getData()); - } - } - - curOffset += elementLength * elSize; - } - } else { - if (useInstancing) { - if (gl.isGL2()) { - gl.getGL2().glDrawElementsInstanced(convertElementMode(mesh.getMode()), - indexBuf.getData().limit(), - convertFormat(indexBuf.getFormat()), - indexBuf.getData(), - count); - } else { - throw new IllegalArgumentException( - "instancing is not supported."); - } - } else { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glDrawRangeElements(convertElementMode(mesh.getMode()), - 0, - vertCount, - indexBuf.getData().limit(), - convertFormat(indexBuf.getFormat()), - 0); - } else { - indexBuf.getData().rewind(); - gl.glDrawElements(convertElementMode(mesh.getMode()), - indexBuf.getData().limit(), - convertFormat(indexBuf.getFormat()), 0); - } - } - } - } - - /** - * *******************************************************************\ |* - * Render Calls *| - \******************************************************************** - */ - private int convertElementMode(Mesh.Mode mode) { - switch (mode) { - case Points: - return GL.GL_POINTS; - case Lines: - return GL.GL_LINES; - case LineLoop: - return GL.GL_LINE_LOOP; - case LineStrip: - return GL.GL_LINE_STRIP; - case Triangles: - return GL.GL_TRIANGLES; - case TriangleFan: - return GL.GL_TRIANGLE_FAN; - case TriangleStrip: - return GL.GL_TRIANGLE_STRIP; - default: - throw new UnsupportedOperationException("Unrecognized mesh mode: " + mode); - } - } - - public void updateVertexArray(Mesh mesh, VertexBuffer instanceData) { - int id = mesh.getId(); - GL gl = GLContext.getCurrentGL(); - - if (id == -1) { - IntBuffer temp = intBuf1; - if (gl.isGL2GL3()) { - gl.getGL2GL3().glGenVertexArrays(1, temp); - } - id = temp.get(0); - mesh.setId(id); - } - - if (context.boundVertexArray != id) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glBindVertexArray(id); - } - context.boundVertexArray = id; - } - - VertexBuffer interleavedData = mesh.getBuffer(Type.InterleavedData); - if (interleavedData != null && interleavedData.isUpdateNeeded()) { - updateBufferData(interleavedData); - } - - if (instanceData != null) { - setVertexAttrib(instanceData, null); - } - - for (VertexBuffer vb : mesh.getBufferList().getArray()) { - if (vb.getBufferType() == Type.InterleavedData - || vb.getUsage() == Usage.CpuOnly // ignore cpu-only buffers - || vb.getBufferType() == Type.Index) { - continue; - } - - if (vb.getStride() == 0) { - // not interleaved - setVertexAttrib(vb); - } else { - // interleaved - setVertexAttrib(vb, interleavedData); - } - } - } - - private void renderMeshVertexArray(Mesh mesh, int lod, int count, VertexBuffer instanceData) { - if (mesh.getId() == -1) { - updateVertexArray(mesh, instanceData); - } else { - // TODO: Check if it was updated - } - - if (context.boundVertexArray != mesh.getId()) { - GL gl = GLContext.getCurrentGL(); - if (gl.isGL2GL3()) { - gl.getGL2GL3().glBindVertexArray(mesh.getId()); - } - context.boundVertexArray = mesh.getId(); - } - -// IntMap buffers = mesh.getBuffers(); - VertexBuffer indices; - if (mesh.getNumLodLevels() > 0) { - indices = mesh.getLodLevel(lod); - } else { - indices = mesh.getBuffer(Type.Index); - } - if (indices != null) { - drawTriangleList(indices, mesh, count); - } else { - drawTriangleArray(mesh.getMode(), count, mesh.getVertexCount()); - } - clearVertexAttribs(); - clearTextureUnits(); - } - - private void renderMeshDefault(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { - - // Here while count is still passed in. Can be removed when/if - // the method is collapsed again. -pspeed - count = Math.max(mesh.getInstanceCount(), count); - - VertexBuffer interleavedData = mesh.getBuffer(Type.InterleavedData); - if (interleavedData != null && interleavedData.isUpdateNeeded()) { - updateBufferData(interleavedData); - } - - VertexBuffer indices; - if (mesh.getNumLodLevels() > 0) { - indices = mesh.getLodLevel(lod); - } else { - indices = mesh.getBuffer(Type.Index); - } - - if (instanceData != null) { - for (VertexBuffer vb : instanceData) { - setVertexAttrib(vb, null); - } - } - - for (VertexBuffer vb : mesh.getBufferList().getArray()) { - if (vb.getBufferType() == Type.InterleavedData - || vb.getUsage() == Usage.CpuOnly // ignore cpu-only buffers - || vb.getBufferType() == Type.Index) { - continue; - } - - if (vb.getStride() == 0) { - // not interleaved - setVertexAttrib(vb); - } else { - // interleaved - setVertexAttrib(vb, interleavedData); - } - } - - if (indices != null) { - drawTriangleList(indices, mesh, count); - } else { - drawTriangleArray(mesh.getMode(), count, mesh.getVertexCount()); - } - clearVertexAttribs(); - clearTextureUnits(); - } - - @Override - public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { - if (mesh.getVertexCount() == 0) { - return; - } - - GL gl = GLContext.getCurrentGL(); - if (context.pointSprite && mesh.getMode() != Mode.Points) { - // XXX: Hack, disable point sprite mode if mesh not in point mode - if (context.boundTextures[0] != null) { - if (context.boundTextureUnit != 0) { - gl.glActiveTexture(GL.GL_TEXTURE0); - context.boundTextureUnit = 0; - } - if (gl.isGL2ES1()) { - gl.glDisable(GL2ES1.GL_POINT_SPRITE); - } - if (gl.isGL2GL3()) { - gl.glDisable(GL2GL3.GL_VERTEX_PROGRAM_POINT_SIZE); - } - context.pointSprite = false; - } - } - - if (context.pointSize != mesh.getPointSize()) { - if (gl.isGL2GL3()) { - gl.getGL2GL3().glPointSize(mesh.getPointSize()); - } - context.pointSize = mesh.getPointSize(); - } - if (context.lineWidth != mesh.getLineWidth()) { - gl.glLineWidth(mesh.getLineWidth()); - context.lineWidth = mesh.getLineWidth(); - } - - statistics.onMeshDrawn(mesh, lod); -// if (gl.isExtensionAvailable("GL_ARB_vertex_array_object")){ -// renderMeshVertexArray(mesh, lod, count); -// }else{ - renderMeshDefault(mesh, lod, count, instanceData); -// } - } - - @Override - public void setMainFrameBufferSrgb(boolean srgb) { - //Gamma correction - if(srgb && caps.contains(Caps.Srgb)){ - GLContext.getCurrentGL().glEnable(GL3.GL_FRAMEBUFFER_SRGB); - logger.log(Level.FINER, "SRGB FrameBuffer enabled (Gamma Correction)"); - }else{ - GLContext.getCurrentGL().glDisable(GL3.GL_FRAMEBUFFER_SRGB); - } - - } - - @Override - public void setLinearizeSrgbImages(boolean linearize) { - linearizeSrgbImages = linearize; - } - - public void readFrameBufferWithFormat(FrameBuffer fb, ByteBuffer byteBuf, Image.Format format) { - throw new UnsupportedOperationException("Not supported yet. URA will make that work seamlessly"); - } -} diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java index 3b90877df..25ad4fc8e 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java @@ -40,7 +40,6 @@ import com.jme3.renderer.RendererException; import com.jme3.renderer.jogl.JoglGL; import com.jme3.renderer.jogl.JoglGLExt; import com.jme3.renderer.jogl.JoglGLFbo; -import com.jme3.renderer.jogl.JoglRenderer; import com.jme3.renderer.opengl.GL2; import com.jme3.renderer.opengl.GL3; import com.jme3.renderer.opengl.GL4; @@ -192,9 +191,7 @@ public abstract class JoglContext implements JmeContext { glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLFbo.class); } - //FIXME uncomment the line below when the unified renderer is ready for the prime time :) - //renderer = new GLRenderer(gl, glext, glfbo); - renderer = new JoglRenderer(); + renderer = new GLRenderer(gl, glext, glfbo); renderer.initialize(); } else { throw new UnsupportedOperationException("Unsupported renderer: " + settings.getRenderer()); From 9f56a45d56c024d0161b6343fe6e97766b5953ce Mon Sep 17 00:00:00 2001 From: Julien Gouesse Date: Tue, 22 Sep 2015 23:11:16 +0200 Subject: [PATCH 62/94] Adds some options into AppSettings for JogAmp's JOGL and JOAL --- .../main/java/com/jme3/system/AppSettings.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/jme3-core/src/main/java/com/jme3/system/AppSettings.java b/jme3-core/src/main/java/com/jme3/system/AppSettings.java index 484458929..6bf826236 100644 --- a/jme3-core/src/main/java/com/jme3/system/AppSettings.java +++ b/jme3-core/src/main/java/com/jme3/system/AppSettings.java @@ -110,6 +110,24 @@ public final class AppSettings extends HashMap { * @see AppSettings#setAudioRenderer(java.lang.String) */ public static final String ANDROID_OPENAL_SOFT = "OpenAL_SOFT"; + + /** + * Use JogAmp's JOGL as the display system + *

+ * N.B: This backend is EXPERIMENTAL + * + * @see AppSettings#setRenderer(java.lang.String) + */ + public static final String JOGL = "JOGL"; + + /** + * Use JogAmp's JOAL as the display system + *

+ * N.B: This backend is EXPERIMENTAL + * + * @see AppSettings#setRenderer(java.lang.String) + */ + public static final String JOAL = "JOAL"; static { defaults.put("Width", 640); From 5f77ff021bb3eb17af4ac400ad0661f96bd3ba6d Mon Sep 17 00:00:00 2001 From: Julien Gouesse Date: Tue, 22 Sep 2015 23:50:28 +0200 Subject: [PATCH 63/94] Fixes the NullPointerException in the demos when using JoglNewtDisplay --- .../src/main/java/com/jme3/system/jogl/JoglNewtDisplay.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtDisplay.java b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtDisplay.java index 84a99e8d2..56eab406a 100644 --- a/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtDisplay.java +++ b/jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtDisplay.java @@ -173,6 +173,8 @@ public class JoglNewtDisplay extends JoglNewtAbstractDisplay { if (waitFor){ waitFor(false); } + if (animator.isAnimating()) + animator.stop(); } public void restart() { From d269839efb1470b3f44c5d201baf49e117783118 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Tue, 22 Sep 2015 21:53:50 -0400 Subject: [PATCH 64/94] FastMath: faster nearestPowerOfTwo and unit test --- .../src/main/java/com/jme3/math/FastMath.java | 19 +++++- .../test/java/com/jme3/math/FastMathTest.java | 59 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 jme3-core/src/test/java/com/jme3/math/FastMathTest.java diff --git a/jme3-core/src/main/java/com/jme3/math/FastMath.java b/jme3-core/src/main/java/com/jme3/math/FastMath.java index d9d944057..4f1a7feed 100644 --- a/jme3-core/src/main/java/com/jme3/math/FastMath.java +++ b/jme3-core/src/main/java/com/jme3/math/FastMath.java @@ -87,8 +87,25 @@ final public class FastMath { return (number > 0) && (number & (number - 1)) == 0; } + /** + * Get the next power of two of the given number. + * + * E.g. for an input 100, this returns 128. + * Returns 1 for all numbers <= 1. + * + * @param number The number to obtain the POT for. + * @return The next power of two. + */ public static int nearestPowerOfTwo(int number) { - return (int) Math.pow(2, Math.ceil(Math.log(number) / Math.log(2))); + number--; + number |= number >> 1; + number |= number >> 2; + number |= number >> 4; + number |= number >> 8; + number |= number >> 16; + number++; + number += (number == 0) ? 1 : 0; + return number; } /** diff --git a/jme3-core/src/test/java/com/jme3/math/FastMathTest.java b/jme3-core/src/test/java/com/jme3/math/FastMathTest.java new file mode 100644 index 000000000..a74390d42 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/math/FastMathTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2009-2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.math; + +import org.junit.Test; + +/** + * Verifies that algorithms in {@link FastMath} are working correctly. + * + * @author Kirill Vainer + */ +public class FastMathTest { + + private int nearestPowerOfTwoSlow(int number) { + return (int) Math.pow(2, Math.ceil(Math.log(number) / Math.log(2))); + } + + @Test + public void testNearestPowerOfTwo() { + for (int i = -100; i < 1; i++) { + assert FastMath.nearestPowerOfTwo(i) == 1; + } + for (int i = 1; i < 10000; i++) { + int nextPowerOf2 = FastMath.nearestPowerOfTwo(i); + assert i <= nextPowerOf2; + assert FastMath.isPowerOfTwo(nextPowerOf2); + assert nextPowerOf2 == nearestPowerOfTwoSlow(i); + } + } +} From efe600c38d0a862a4034a7b8ca8aeb42f247cbea Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Tue, 22 Sep 2015 21:57:42 -0400 Subject: [PATCH 65/94] GLRenderer: put max anisotropy into limits map --- jme3-core/src/main/java/com/jme3/renderer/Limits.java | 2 ++ jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java | 1 + .../src/main/java/com/jme3/renderer/opengl/GLRenderer.java | 1 + 3 files changed, 4 insertions(+) diff --git a/jme3-core/src/main/java/com/jme3/renderer/Limits.java b/jme3-core/src/main/java/com/jme3/renderer/Limits.java index 86cddb178..81db88f5e 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Limits.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Limits.java @@ -77,4 +77,6 @@ public enum Limits { DepthTextureSamples, VertexUniformVectors, + + TextureAnisotropy, } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java index 27f0eb8bd..c77ff6449 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java @@ -68,6 +68,7 @@ public interface GLExt { public static final int GL_MAX_DEPTH_TEXTURE_SAMPLES = 0x910F; public static final int GL_MAX_DRAW_BUFFERS_ARB = 0x8824; public static final int GL_MAX_SAMPLES_EXT = 0x8D57; + public static final int GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT = 0x84FF; public static final int GL_MULTISAMPLE_ARB = 0x809D; public static final int GL_NUM_PROGRAM_BINARY_FORMATS = 0x87FE; public static final int GL_PIXEL_PACK_BUFFER_ARB = 0x88EB; diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 276453117..ee7f7a560 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -374,6 +374,7 @@ public class GLRenderer implements Renderer { if (hasExtension("GL_EXT_texture_filter_anisotropic")) { caps.add(Caps.TextureFilterAnisotropic); + limits.put(Limits.TextureAnisotropy, getInteger(GLExt.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT)); } if (hasExtension("GL_EXT_framebuffer_object") From aa54947ff36ac2591abef4c8e10c09c97cdddf72 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Tue, 22 Sep 2015 22:01:17 -0400 Subject: [PATCH 66/94] GLRenderer: cleanup to shadow compare mode Store compare mode in LastTextureState instead of on Texture object --- .../java/com/jme3/renderer/opengl/GL2.java | 2 +- .../com/jme3/renderer/opengl/GLRenderer.java | 52 +++++++++---------- .../main/java/com/jme3/texture/Texture.java | 14 +---- .../jme3/texture/image/LastTextureState.java | 9 +++- 4 files changed, 33 insertions(+), 44 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL2.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL2.java index db09fdd02..abf6a77d7 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL2.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL2.java @@ -44,7 +44,7 @@ public interface GL2 extends GL { public static final int GL_ALPHA_TEST = 0xBC0; public static final int GL_BGR = 0x80E0; public static final int GL_BGRA = 0x80E1; - public static final int GL_COMPARE_R_TO_TEXTURE = 0x884E; + public static final int GL_COMPARE_REF_TO_TEXTURE = 0x884E; public static final int GL_DEPTH_COMPONENT24 = 0x81A6; public static final int GL_DEPTH_COMPONENT32 = 0x81A7; public static final int GL_DEPTH_TEXTURE_MODE = 0x884B; diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index ee7f7a560..7b40994ad 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -51,7 +51,9 @@ import com.jme3.texture.FrameBuffer; import com.jme3.texture.FrameBuffer.RenderBuffer; import com.jme3.texture.Image; import com.jme3.texture.Texture; +import com.jme3.texture.Texture.ShadowCompareMode; import com.jme3.texture.Texture.WrapAxis; +import com.jme3.texture.image.LastTextureState; import com.jme3.util.BufferUtils; import com.jme3.util.ListMap; import com.jme3.util.MipMapGenerator; @@ -1842,77 +1844,71 @@ public class GLRenderer implements Renderer { int target = convertTextureType(tex.getType(), image != null ? image.getMultiSamples() : 1, -1); boolean haveMips = true; - if (image != null) { haveMips = image.isGeneratedMipmapsRequired() || image.hasMipmaps(); } + + LastTextureState curState = image.getLastTextureState(); - // filter things - if (image.getLastTextureState().magFilter != tex.getMagFilter()) { - int magFilter = convertMagFilter(tex.getMagFilter()); + if (curState.magFilter != tex.getMagFilter()) { bindTextureAndUnit(target, image, unit); - gl.glTexParameteri(target, GL.GL_TEXTURE_MAG_FILTER, magFilter); - image.getLastTextureState().magFilter = tex.getMagFilter(); + gl.glTexParameteri(target, GL.GL_TEXTURE_MAG_FILTER, convertMagFilter(tex.getMagFilter())); + curState.magFilter = tex.getMagFilter(); } - if (image.getLastTextureState().minFilter != tex.getMinFilter()) { - int minFilter = convertMinFilter(tex.getMinFilter(), haveMips); + if (curState.minFilter != tex.getMinFilter()) { bindTextureAndUnit(target, image, unit); - gl.glTexParameteri(target, GL.GL_TEXTURE_MIN_FILTER, minFilter); - image.getLastTextureState().minFilter = tex.getMinFilter(); + gl.glTexParameteri(target, GL.GL_TEXTURE_MIN_FILTER, convertMinFilter(tex.getMinFilter(), haveMips)); + curState.minFilter = tex.getMinFilter(); } if (caps.contains(Caps.TextureFilterAnisotropic) - && image.getLastTextureState().anisoFilter != tex.getAnisotropicFilter()) { + && curState.anisoFilter != tex.getAnisotropicFilter()) { bindTextureAndUnit(target, image, unit); gl.glTexParameterf(target, GLExt.GL_TEXTURE_MAX_ANISOTROPY_EXT, tex.getAnisotropicFilter()); - image.getLastTextureState().anisoFilter = tex.getAnisotropicFilter(); + curState.anisoFilter = tex.getAnisotropicFilter(); } - // repeat modes switch (tex.getType()) { case ThreeDimensional: case CubeMap: // cubemaps use 3D coords - if (gl2 != null && image.getLastTextureState().rWrap != tex.getWrap(WrapAxis.R)) { + if (gl2 != null && curState.rWrap != tex.getWrap(WrapAxis.R)) { bindTextureAndUnit(target, image, unit); gl2.glTexParameteri(target, GL2.GL_TEXTURE_WRAP_R, convertWrapMode(tex.getWrap(WrapAxis.R))); - image.getLastTextureState().rWrap = tex.getWrap(WrapAxis.R); + curState.rWrap = tex.getWrap(WrapAxis.R); } //There is no break statement on purpose here case TwoDimensional: case TwoDimensionalArray: - if (image.getLastTextureState().tWrap != tex.getWrap(WrapAxis.T)) { + if (curState.tWrap != tex.getWrap(WrapAxis.T)) { bindTextureAndUnit(target, image, unit); gl.glTexParameteri(target, GL.GL_TEXTURE_WRAP_T, convertWrapMode(tex.getWrap(WrapAxis.T))); image.getLastTextureState().tWrap = tex.getWrap(WrapAxis.T); } - if (image.getLastTextureState().sWrap != tex.getWrap(WrapAxis.S)) { + if (curState.sWrap != tex.getWrap(WrapAxis.S)) { bindTextureAndUnit(target, image, unit); gl.glTexParameteri(target, GL.GL_TEXTURE_WRAP_S, convertWrapMode(tex.getWrap(WrapAxis.S))); - image.getLastTextureState().sWrap = tex.getWrap(WrapAxis.S); + curState.sWrap = tex.getWrap(WrapAxis.S); } break; default: throw new UnsupportedOperationException("Unknown texture type: " + tex.getType()); } - if (tex.isNeedCompareModeUpdate() && gl2 != null) { - // R to Texture compare mode - if (tex.getShadowCompareMode() != Texture.ShadowCompareMode.Off) { - bindTextureAndUnit(target, image, unit); - gl2.glTexParameteri(target, GL2.GL_TEXTURE_COMPARE_MODE, GL2.GL_COMPARE_R_TO_TEXTURE); - gl2.glTexParameteri(target, GL2.GL_DEPTH_TEXTURE_MODE, GL2.GL_INTENSITY); - if (tex.getShadowCompareMode() == Texture.ShadowCompareMode.GreaterOrEqual) { + ShadowCompareMode texCompareMode = tex.getShadowCompareMode(); + if (gl2 != null && curState.shadowCompareMode != texCompareMode) { + bindTextureAndUnit(target, image, unit); + if (texCompareMode != ShadowCompareMode.Off) { + gl2.glTexParameteri(target, GL2.GL_TEXTURE_COMPARE_MODE, GL2.GL_COMPARE_REF_TO_TEXTURE); + if (texCompareMode == ShadowCompareMode.GreaterOrEqual) { gl2.glTexParameteri(target, GL2.GL_TEXTURE_COMPARE_FUNC, GL.GL_GEQUAL); } else { gl2.glTexParameteri(target, GL2.GL_TEXTURE_COMPARE_FUNC, GL.GL_LEQUAL); } } else { - bindTextureAndUnit(target, image, unit); - //restoring default value gl2.glTexParameteri(target, GL2.GL_TEXTURE_COMPARE_MODE, GL.GL_NONE); } - tex.compareModeUpdated(); + curState.shadowCompareMode = texCompareMode; } // If at this point we didn't bind the texture, bind it now diff --git a/jme3-core/src/main/java/com/jme3/texture/Texture.java b/jme3-core/src/main/java/com/jme3/texture/Texture.java index 582d06565..6be00700b 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Texture.java +++ b/jme3-core/src/main/java/com/jme3/texture/Texture.java @@ -316,7 +316,6 @@ public abstract class Texture implements CloneableSmartAsset, Savable, Cloneable private MinFilter minificationFilter = MinFilter.BilinearNoMipMaps; private MagFilter magnificationFilter = MagFilter.Bilinear; private ShadowCompareMode shadowCompareMode = ShadowCompareMode.Off; - private boolean needCompareModeUpdate = false; private int anisotropicFilter; /** @@ -404,7 +403,6 @@ public abstract class Texture implements CloneableSmartAsset, Savable, Cloneable "compareMode can not be null."); } this.shadowCompareMode = compareMode; - needCompareModeUpdate = true; } /** @@ -489,7 +487,7 @@ public abstract class Texture implements CloneableSmartAsset, Savable, Cloneable /** * @return the anisotropic filtering level for this texture. Default value * is 0 (use value from config), - * 1 means 1x (no anisotrophy), 2 means x2, 4 is x4, etc. + * 1 means 1x (no anisotropy), 2 means x2, 4 is x4, etc. */ public int getAnisotropicFilter() { return anisotropicFilter; @@ -636,14 +634,4 @@ public abstract class Texture implements CloneableSmartAsset, Savable, Cloneable magnificationFilter = capsule.readEnum("magnificationFilter", MagFilter.class, MagFilter.Bilinear); } - - public boolean isNeedCompareModeUpdate() { - return needCompareModeUpdate; - } - - public void compareModeUpdated() { - this.needCompareModeUpdate = false; - } - - } diff --git a/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java b/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java index 3b85563ef..32c1b718b 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java @@ -45,10 +45,11 @@ public final class LastTextureState { public Texture.WrapMode sWrap, tWrap, rWrap; public Texture.MagFilter magFilter; public Texture.MinFilter minFilter; - public int anisoFilter = 0; + public int anisoFilter; + public Texture.ShadowCompareMode shadowCompareMode; public LastTextureState() { - // All parameters initialized to null (meaning unset). + reset(); } public void reset() { @@ -58,5 +59,9 @@ public final class LastTextureState { magFilter = null; minFilter = null; anisoFilter = 0; + + // The default in OpenGL is OFF, so we avoid setting this per texture + // if its not used. + shadowCompareMode = Texture.ShadowCompareMode.Off; } } From 62186362a8ba68846b6cfb77fc62ca9d1810ef7e Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Tue, 22 Sep 2015 22:02:12 -0400 Subject: [PATCH 67/94] GLDebugOutputHandler: dump stack on debug messages --- .../java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java index c8f329f13..d943fe6ee 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java +++ b/jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java @@ -73,6 +73,7 @@ class LwjglGLDebugOutputHandler implements ARBDebugOutputCallback.Handler { String severityStr = constMap.get(severity); System.err.println(String.format(MESSAGE_FORMAT, id, sourceStr, typeStr, severityStr, message)); + Thread.dumpStack(); } } From cba39fa0ffa49092fe20c43a609b109bdae86bed Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Tue, 22 Sep 2015 22:02:55 -0400 Subject: [PATCH 68/94] GLRenderer: make the class final --- .../src/main/java/com/jme3/renderer/opengl/GLRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 7b40994ad..69ef39b2c 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -70,7 +70,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import jme3tools.shader.ShaderDebug; -public class GLRenderer implements Renderer { +public final class GLRenderer implements Renderer { private static final Logger logger = Logger.getLogger(GLRenderer.class.getName()); private static final boolean VALIDATE_SHADER = false; From 4fef16ee9fef4539aff0067842cefdc97920aeb9 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Tue, 22 Sep 2015 22:04:25 -0400 Subject: [PATCH 69/94] GLRenderer: fix incorrect gl3 check Should check against caps; since gl3 is always set on desktop regardless if GL3 is available or not. Also add FBO blit support if we have GL3. --- .../src/main/java/com/jme3/renderer/opengl/GLRenderer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 69ef39b2c..50e603d2f 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -380,14 +380,14 @@ public final class GLRenderer implements Renderer { } if (hasExtension("GL_EXT_framebuffer_object") - || gl3 != null + || caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES20)) { caps.add(Caps.FrameBuffer); limits.put(Limits.RenderBufferSize, getInteger(GLFbo.GL_MAX_RENDERBUFFER_SIZE_EXT)); limits.put(Limits.FrameBufferAttachments, getInteger(GLFbo.GL_MAX_COLOR_ATTACHMENTS_EXT)); - if (hasExtension("GL_EXT_framebuffer_blit")) { + if (hasExtension("GL_EXT_framebuffer_blit") || caps.contains(Caps.OpenGL30)) { caps.add(Caps.FrameBufferBlit); } @@ -406,7 +406,7 @@ public final class GLRenderer implements Renderer { } } - if (hasExtension("GL_ARB_draw_buffers") || gl3 != null) { + if (hasExtension("GL_ARB_draw_buffers") || caps.contains(Caps.OpenGL30)) { limits.put(Limits.FrameBufferMrtAttachments, getInteger(GLExt.GL_MAX_DRAW_BUFFERS_ARB)); if (limits.get(Limits.FrameBufferMrtAttachments) > 1) { caps.add(Caps.FrameBufferMRT); From 4a37a8f8514c67613c0874ef1ff15825a939e3f1 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Tue, 22 Sep 2015 22:09:38 -0400 Subject: [PATCH 70/94] FXAA: quality regression fix FXAA wants input texture to use bilinear filtering so it can be smoothed further, so allow filters to request bilinear filtering. --- jme3-core/src/main/java/com/jme3/post/Filter.java | 13 +++++++++++++ .../java/com/jme3/post/FilterPostProcessor.java | 12 ++++++++++++ .../main/java/com/jme3/post/filters/FXAAFilter.java | 6 ++++++ 3 files changed, 31 insertions(+) diff --git a/jme3-core/src/main/java/com/jme3/post/Filter.java b/jme3-core/src/main/java/com/jme3/post/Filter.java index a6e2e3c01..feaacbe5d 100644 --- a/jme3-core/src/main/java/com/jme3/post/Filter.java +++ b/jme3-core/src/main/java/com/jme3/post/Filter.java @@ -409,6 +409,19 @@ public abstract class Filter implements Savable { return true; } + /** + * Override this method and return true if you want the scene (input) texture + * to use bilinear filtering or false to use nearest filtering. + * + * Typically filters that perform samples in between pixels + * should enable filtering. + * + * @return true to use linear filtering, false to use nearest filtering. + */ + protected boolean isRequiresBilinear() { + return false; + } + /** * returns the list of the postRender passes * @return diff --git a/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java b/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java index b28aecece..2f52eb425 100644 --- a/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java +++ b/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java @@ -38,6 +38,7 @@ import com.jme3.renderer.*; import com.jme3.renderer.queue.RenderQueue; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; import com.jme3.ui.Picture; import com.jme3.util.SafeArrayList; @@ -284,6 +285,12 @@ public class FilterPostProcessor implements SceneProcessor, Savable { mat.clearParam("NumSamples"); } } + + boolean wantsBilinear = filter.isRequiresBilinear(); + if (wantsBilinear) { + tex.setMagFilter(Texture.MagFilter.Bilinear); + tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); + } buff = outputBuffer; if (i != lastFilterIndex) { @@ -293,6 +300,11 @@ public class FilterPostProcessor implements SceneProcessor, Savable { } renderProcessing(r, buff, mat); filter.postFilter(r, buff); + + if (wantsBilinear) { + tex.setMagFilter(Texture.MagFilter.Nearest); + tex.setMinFilter(Texture.MinFilter.NearestNoMipMaps); + } } } } diff --git a/jme3-effects/src/main/java/com/jme3/post/filters/FXAAFilter.java b/jme3-effects/src/main/java/com/jme3/post/filters/FXAAFilter.java index 7b1dfb37a..37fec3bcd 100644 --- a/jme3-effects/src/main/java/com/jme3/post/filters/FXAAFilter.java +++ b/jme3-effects/src/main/java/com/jme3/post/filters/FXAAFilter.java @@ -69,6 +69,12 @@ public class FXAAFilter extends Filter { protected Material getMaterial() { return material; } + + @Override + protected boolean isRequiresBilinear() { + // FXAA wants the input texture to be filtered. + return true; + } public void setSpanMax(float spanMax) { this.spanMax = spanMax; From 01227d31b022302c986acbeb0cb4a3af359c30eb Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Tue, 22 Sep 2015 22:10:16 -0400 Subject: [PATCH 71/94] Lighting: fix colorramp feature --- .../main/resources/Common/MatDefs/Light/Lighting.frag | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.frag b/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.frag index d2ade5199..0aa4d3f36 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.frag +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.frag @@ -154,8 +154,9 @@ void main(){ #ifdef VERTEX_LIGHTING vec2 light = vertexLightValues.xy; #ifdef COLORRAMP - light.x = texture2D(m_ColorRamp, vec2(light.x, 0.0)).r; - light.y = texture2D(m_ColorRamp, vec2(light.y, 0.0)).r; + diffuseColor.rgb *= texture2D(m_ColorRamp, vec2(light.x, 0.0)).rgb; + specularColor.rgb *= texture2D(m_ColorRamp, vec2(light.y, 0.0)).rgb; + light.xy = vec2(1.0); #endif gl_FragColor.rgb = AmbientSum * diffuseColor.rgb + @@ -183,8 +184,9 @@ void main(){ vec2 light = computeLighting(normal, viewDir, lightDir.xyz, lightDir.w * spotFallOff, m_Shininess) ; #ifdef COLORRAMP - diffuseColor.rgb *= texture2D(m_ColorRamp, vec2(light.x, 0.0)).rgb; - specularColor.rgb *= texture2D(m_ColorRamp, vec2(light.y, 0.0)).rgb; + diffuseColor.rgb *= texture2D(m_ColorRamp, vec2(light.x, 0.0)).rgb; + specularColor.rgb *= texture2D(m_ColorRamp, vec2(light.y, 0.0)).rgb; + light.xy = vec2(1.0); #endif // Workaround, since it is not possible to modify varying variables From e4f3c06b1df56999c582b5c770c593852108bcc9 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Tue, 22 Sep 2015 22:12:13 -0400 Subject: [PATCH 72/94] ListMap: create unit test --- .../src/main/java/com/jme3/util/ListMap.java | 15 ---- .../test/java/com/jme3/util/ListMapTest.java | 73 +++++++++++++++++++ 2 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 jme3-core/src/test/java/com/jme3/util/ListMapTest.java diff --git a/jme3-core/src/main/java/com/jme3/util/ListMap.java b/jme3-core/src/main/java/com/jme3/util/ListMap.java index 0de4d3f75..379fff741 100644 --- a/jme3-core/src/main/java/com/jme3/util/ListMap.java +++ b/jme3-core/src/main/java/com/jme3/util/ListMap.java @@ -43,21 +43,6 @@ import java.util.Map.Entry; */ public final class ListMap extends AbstractMap implements Cloneable, Serializable { - public static void main(String[] args){ - Map map = new ListMap(); - map.put( "bob", "hello"); - System.out.println(map.get("bob")); - map.remove("bob"); - System.out.println(map.size()); - - map.put("abc", "1"); - map.put("def", "2"); - map.put("ghi", "3"); - map.put("jkl", "4"); - map.put("mno", "5"); - System.out.println(map.get("ghi")); - } - private final static class ListMapEntry implements Map.Entry, Cloneable { private final K key; diff --git a/jme3-core/src/test/java/com/jme3/util/ListMapTest.java b/jme3-core/src/test/java/com/jme3/util/ListMapTest.java new file mode 100644 index 000000000..5f4cf3eee --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/util/ListMapTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2009-2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.util; + +import java.util.Map.Entry; +import org.junit.Test; + +/** + * Check if the {@link ListMap} class is working correctly. + * + * @author Kirill Vainer + */ +public class ListMapTest { + + @Test + public void testListMap() { + ListMap listMap = new ListMap(); + listMap.put("bob", "hello"); + assert "hello".equals(listMap.get("bob")); + assert "hello".equals(listMap.remove("bob")); + assert listMap.size() == 0; + assert listMap.isEmpty(); + + listMap.put("abc", "1"); + listMap.put("def", "2"); + listMap.put("ghi", "3"); + listMap.put("jkl", "4"); + listMap.put("mno", "5"); + assert "3".equals(listMap.get("ghi")); + assert listMap.size() == 5; + assert !listMap.isEmpty(); + + // check iteration order, should be consistent + for (int i = 0; i < listMap.size(); i++) { + String expectedValue = Integer.toString(i + 1); + String key = listMap.getKey(i); + String value = listMap.getValue(i); + Entry entry = listMap.getEntry(i); + assert key.equals(entry.getKey()); + assert value.equals(entry.getValue()); + assert expectedValue.equals(value); + } + } +} From 2eb2cdac604abcbd94a44ceb366cb2fec5f9314c Mon Sep 17 00:00:00 2001 From: alexVengrovsk Date: Wed, 23 Sep 2015 12:22:11 +0300 Subject: [PATCH 73/94] Delete close() of ByteArrayOutputStream type objecs According to the Oracle's docummentation: "Closing a ByteArrayOutputStream has no effect. The methods in this class can be called after the stream has been closed without generating an IOException." (http://docs.oracle.com/javase/7/docs/api/java/io/ByteArrayOutputStream.html#close()) --- .../main/java/com/jme3/app/state/MjpegFileWriter.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/jme3-desktop/src/main/java/com/jme3/app/state/MjpegFileWriter.java b/jme3-desktop/src/main/java/com/jme3/app/state/MjpegFileWriter.java index 4c4cd6562..b3d751465 100644 --- a/jme3-desktop/src/main/java/com/jme3/app/state/MjpegFileWriter.java +++ b/jme3-desktop/src/main/java/com/jme3/app/state/MjpegFileWriter.java @@ -205,7 +205,6 @@ public class MjpegFileWriter { baos.write(fcc3); baos.write(intBytes(swapInt(listSize))); baos.write(fcc4); - baos.close(); return baos.toByteArray(); } @@ -275,7 +274,6 @@ public class MjpegFileWriter { baos.write(intBytes(swapInt(dwReserved[1]))); baos.write(intBytes(swapInt(dwReserved[2]))); baos.write(intBytes(swapInt(dwReserved[3]))); - baos.close(); return baos.toByteArray(); } @@ -295,7 +293,6 @@ public class MjpegFileWriter { baos.write(fcc); baos.write(intBytes(swapInt(size))); baos.write(fcc2); - baos.close(); return baos.toByteArray(); } @@ -365,7 +362,6 @@ public class MjpegFileWriter { baos.write(intBytes(swapInt(top))); baos.write(intBytes(swapInt(right))); baos.write(intBytes(swapInt(bottom))); - baos.close(); return baos.toByteArray(); } @@ -420,7 +416,6 @@ public class MjpegFileWriter { baos.write(intBytes(swapInt(biYPelsPerMeter))); baos.write(intBytes(swapInt(biClrUsed))); baos.write(intBytes(swapInt(biClrImportant))); - baos.close(); return baos.toByteArray(); } @@ -445,7 +440,6 @@ public class MjpegFileWriter { baos.write(fcc); baos.write(intBytes(swapInt(listSize))); baos.write(fcc2); - baos.close(); return baos.toByteArray(); } @@ -480,8 +474,6 @@ public class MjpegFileWriter { baos.write(in.toBytes()); } - baos.close(); - return baos.toByteArray(); } } @@ -504,7 +496,6 @@ public class MjpegFileWriter { baos.write(intBytes(swapInt(dwFlags))); baos.write(intBytes(swapInt(dwOffset))); baos.write(intBytes(swapInt(dwSize))); - baos.close(); return baos.toByteArray(); } @@ -525,7 +516,6 @@ public class MjpegFileWriter { baos.write(fcc); baos.write(intBytes(swapInt(size))); baos.write(data); - baos.close(); return baos.toByteArray(); } @@ -552,7 +542,6 @@ public class MjpegFileWriter { imgWrtr.write(null, new IIOImage(bi, null, null), jpgWrtPrm); imgOutStrm.close(); - baos.close(); return baos.toByteArray(); } } From f4706373618215e3868f9779b68f8da4918ebd37 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 24 Sep 2015 11:02:47 +0100 Subject: [PATCH 74/94] Fixed a build issue regarding lwjgl 3.x by now using 3.0.0a for longer stability as 3.0.0b-SNAPSHOT is a moving target and APIs are changing constantly. --- jme3-lwjgl3/build.gradle | 8 ++++---- .../main/java/com/jme3/system/lwjgl/LwjglContext.java | 10 ++++++---- .../main/java/com/jme3/system/lwjgl/LwjglWindow.java | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/jme3-lwjgl3/build.gradle b/jme3-lwjgl3/build.gradle index 3c7e4e646..e81175700 100644 --- a/jme3-lwjgl3/build.gradle +++ b/jme3-lwjgl3/build.gradle @@ -11,8 +11,8 @@ repositories { dependencies { compile project(':jme3-core') compile project(':jme3-desktop') - compile 'org.lwjgl:lwjgl:3.0.0b-SNAPSHOT' - compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0b-SNAPSHOT', classifier: 'natives-windows' - compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0b-SNAPSHOT', classifier: 'natives-linux' - compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0b-SNAPSHOT', classifier: 'natives-osx' + compile 'org.lwjgl:lwjgl:3.0.0a' + compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0a', classifier: 'natives-windows' + compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0a', classifier: 'natives-linux' + compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0a', classifier: 'natives-osx' } diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java index 7f62a447e..f4d9a7b57 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -42,11 +42,13 @@ import com.jme3.renderer.lwjgl.LwjglGLExt; import com.jme3.renderer.lwjgl.LwjglGLFboEXT; import com.jme3.renderer.lwjgl.LwjglGLFboGL3; import com.jme3.renderer.opengl.*; -import com.jme3.renderer.opengl.GL; import com.jme3.system.*; import org.lwjgl.Sys; import org.lwjgl.glfw.GLFW; -import org.lwjgl.opengl.*; +import org.lwjgl.opengl.ARBDebugOutput; +import org.lwjgl.opengl.ARBFramebufferObject; +import org.lwjgl.opengl.ContextCapabilities; +import org.lwjgl.opengl.EXTFramebufferMultisample; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; @@ -132,7 +134,7 @@ public abstract class LwjglContext implements JmeContext { } protected void initContextFirstTime() { - final GLCapabilities capabilities = createCapabilities(settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)); + final ContextCapabilities capabilities = createCapabilities(settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)); if (!capabilities.OpenGL20) { throw new RendererException("OpenGL 2.0 or higher is required for jMonkeyEngine"); @@ -230,7 +232,7 @@ public abstract class LwjglContext implements JmeContext { while (created.get() != createdVal) { try { createdLock.wait(); - } catch (InterruptedException ex) { + } catch (InterruptedException ignored) { } } } diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java index 2f00eb700..e1f4dcf73 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java @@ -99,7 +99,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { /** * Set the title if its a windowed display * - * @param title + * @param title the title to set */ public void setTitle(final String title) { if (created.get() && window != -1) { @@ -121,7 +121,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { /** * Apply the settings, changing resolution, etc. * - * @param settings + * @param settings the settings to apply when creating the context. */ protected void createContext(final AppSettings settings) { glfwSetErrorCallback(errorCallback = new GLFWErrorCallback() { From 109c5e80cf0645a5fef16f4aee89831cf5b9b302 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 24 Sep 2015 13:41:22 +0100 Subject: [PATCH 75/94] Fixed #316 where some post processing effects were not working when using OPENGL_3 renderer due to an error in the fragment shader. --- .../main/resources/Common/MatDefs/Post/CrossHatch15.frag | 6 ++++-- .../main/resources/Common/MatDefs/Post/DepthOfField15.frag | 6 ++++-- .../src/main/resources/Common/MatDefs/Post/Fade15.frag | 3 ++- .../src/main/resources/Common/MatDefs/Post/Fog15.frag | 3 ++- .../src/main/resources/Common/MatDefs/Post/Overlay15.frag | 3 ++- .../main/resources/Common/MatDefs/Post/Posterization15.frag | 5 +++-- .../main/resources/Common/MatDefs/Post/bloomFinal15.frag | 3 ++- .../src/main/resources/Common/MatDefs/SSAO/ssaoBlur15.frag | 3 ++- 8 files changed, 21 insertions(+), 11 deletions(-) diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/CrossHatch15.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/CrossHatch15.frag index 8daa4f7fd..842484604 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/CrossHatch15.frag +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/CrossHatch15.frag @@ -17,7 +17,9 @@ uniform float m_Luminance5; uniform float m_LineDistance; uniform float m_LineThickness; - + +out vec4 fragColor; + void main() { vec4 texVal = getColor(m_Texture, texCoord); float linePixel = 0; @@ -49,5 +51,5 @@ void main() { // Mix paper color with existing color information vec4 paperColor = mix(m_PaperColor, texVal, m_ColorInfluencePaper); - gl_FragColor = mix(paperColor, lineColor, linePixel); + fragColor = mix(paperColor, lineColor, linePixel); } \ No newline at end of file diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/DepthOfField15.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/DepthOfField15.frag index 80cf3c530..de3c586ae 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/DepthOfField15.frag +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/DepthOfField15.frag @@ -11,6 +11,8 @@ uniform float m_YScale; vec2 m_NearFar = vec2( 0.1, 1000.0 ); +out vec4 fragColor; + void main() { vec4 texVal = getColor( m_Texture, texCoord ); @@ -44,7 +46,7 @@ void main() { if( unfocus < 0.2 ) { // If we are mostly in focus then don't bother with the // convolution filter - gl_FragColor = texVal; + fragColor = texVal; } else { // Perform a wide convolution filter and we scatter it // a bit to avoid some texture look-ups. Instead of @@ -83,7 +85,7 @@ void main() { sum = sum / 12.0; - gl_FragColor = mix( texVal, sum, unfocus ); + fragColor = mix( texVal, sum, unfocus ); // I used this for debugging the range // gl_FragColor.r = unfocus; diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Fade15.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/Fade15.frag index c99de34ad..7bd2d4882 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/Fade15.frag +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/Fade15.frag @@ -4,8 +4,9 @@ uniform COLORTEXTURE m_Texture; uniform float m_Value; in vec2 texCoord; +out vec4 fragColor; void main() { vec4 texVal = getColor(m_Texture, texCoord); - gl_FragColor = texVal * m_Value; + fragColor = texVal * m_Value; } \ No newline at end of file diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Fog15.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/Fog15.frag index 65a340723..7a1f756be 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/Fog15.frag +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/Fog15.frag @@ -8,6 +8,7 @@ uniform float m_FogDensity; uniform float m_FogDistance; in vec2 texCoord; +out vec4 fragColor; vec2 m_FrustumNearFar=vec2(1.0,m_FogDistance); const float LOG2 = 1.442695; @@ -19,6 +20,6 @@ void main() { float fogFactor = exp2( -m_FogDensity * m_FogDensity * depth * depth * LOG2 ); fogFactor = clamp(fogFactor, 0.0, 1.0); - gl_FragColor =mix(m_FogColor,texVal,fogFactor); + fragColor =mix(m_FogColor,texVal,fogFactor); } \ No newline at end of file diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Overlay15.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/Overlay15.frag index 7dc4d1e4b..d6609f38f 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/Overlay15.frag +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/Overlay15.frag @@ -3,9 +3,10 @@ uniform COLORTEXTURE m_Texture; uniform vec4 m_Color; in vec2 texCoord; +out vec4 fragColor; void main() { vec4 texVal = getColor(m_Texture, texCoord); - gl_FragColor = texVal * m_Color; + fragColor = texVal * m_Color; } diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Posterization15.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/Posterization15.frag index f55ac5bf2..5d8d7072b 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/Posterization15.frag +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/Posterization15.frag @@ -2,7 +2,8 @@ uniform COLORTEXTURE m_Texture; in vec2 texCoord; - +out vec4 fragColor; + uniform int m_NumColors; uniform float m_Gamma; uniform float m_Strength; @@ -16,5 +17,5 @@ void main() { texVal = texVal / m_NumColors; texVal = pow(texVal, vec4(1.0/m_Gamma)); - gl_FragColor = mix(getColor(m_Texture, texCoord), texVal, m_Strength); + fragColor = mix(getColor(m_Texture, texCoord), texVal, m_Strength); } \ No newline at end of file diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/bloomFinal15.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/bloomFinal15.frag index 34268c565..b8f322d11 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/bloomFinal15.frag +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/bloomFinal15.frag @@ -6,10 +6,11 @@ uniform sampler2D m_BloomTex; uniform float m_BloomIntensity; in vec2 texCoord; +out vec4 fragColor; void main(){ vec4 colorRes = getColor(m_Texture,texCoord); vec4 bloom = texture2D(m_BloomTex, texCoord); - gl_FragColor = bloom * m_BloomIntensity + colorRes; + fragColor = bloom * m_BloomIntensity + colorRes; } diff --git a/jme3-effects/src/main/resources/Common/MatDefs/SSAO/ssaoBlur15.frag b/jme3-effects/src/main/resources/Common/MatDefs/SSAO/ssaoBlur15.frag index c1112b23b..be2e6bcde 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/SSAO/ssaoBlur15.frag +++ b/jme3-effects/src/main/resources/Common/MatDefs/SSAO/ssaoBlur15.frag @@ -10,6 +10,7 @@ uniform float m_YScale; uniform vec2 m_FrustumNearFar; in vec2 texCoord; +out vec4 fragColor; vec4 getResult(vec4 color){ @@ -125,7 +126,7 @@ float readDepth(in vec2 uv){ void main(){ // float depth =texture2D(m_DepthTexture,uv).r; - gl_FragColor=getResult(convolutionFilter()); + fragColor=getResult(convolutionFilter()); // gl_FragColor=getResult(bilateralFilter()); // gl_FragColor=getColor(m_SSAOMap,texCoord); From 5da9fa6bc204e306beec7c10264c6abb675df004 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 24 Sep 2015 13:51:46 +0100 Subject: [PATCH 76/94] Changed the default app title in AppSettings to use the full name string from JmeVersion. This way no more manual changing of this will be needed for future versions. This also closes #320 which highlighted this issue. Thanks @8Keep. --- jme3-core/src/main/java/com/jme3/system/AppSettings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/system/AppSettings.java b/jme3-core/src/main/java/com/jme3/system/AppSettings.java index 6bf826236..c98845e41 100644 --- a/jme3-core/src/main/java/com/jme3/system/AppSettings.java +++ b/jme3-core/src/main/java/com/jme3/system/AppSettings.java @@ -138,7 +138,7 @@ public final class AppSettings extends HashMap { defaults.put("StencilBits", 0); defaults.put("Samples", 0); defaults.put("Fullscreen", false); - defaults.put("Title", "jMonkey Engine 3.0"); + defaults.put("Title", JmeVersion.FULL_NAME); defaults.put("Renderer", LWJGL_OPENGL2); defaults.put("AudioRenderer", LWJGL_OPENAL); defaults.put("DisableJoysticks", true); From 14349695a963940b9d25418af5b36224b26b89c3 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Fri, 25 Sep 2015 10:32:11 +0100 Subject: [PATCH 77/94] The LWJGL 3 renderer was missing a call to GLContext.createFromCurrent(), sorted now. --- .../src/main/java/com/jme3/system/lwjgl/LwjglContext.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java index f4d9a7b57..9974693ce 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -42,13 +42,11 @@ import com.jme3.renderer.lwjgl.LwjglGLExt; import com.jme3.renderer.lwjgl.LwjglGLFboEXT; import com.jme3.renderer.lwjgl.LwjglGLFboGL3; import com.jme3.renderer.opengl.*; +import com.jme3.renderer.opengl.GL; import com.jme3.system.*; import org.lwjgl.Sys; import org.lwjgl.glfw.GLFW; -import org.lwjgl.opengl.ARBDebugOutput; -import org.lwjgl.opengl.ARBFramebufferObject; -import org.lwjgl.opengl.ContextCapabilities; -import org.lwjgl.opengl.EXTFramebufferMultisample; +import org.lwjgl.opengl.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; @@ -134,6 +132,7 @@ public abstract class LwjglContext implements JmeContext { } protected void initContextFirstTime() { + GLContext.createFromCurrent(); final ContextCapabilities capabilities = createCapabilities(settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)); if (!capabilities.OpenGL20) { From f1a0125dc232473481d7ab8a3ef5dfe9f8cd8fcc Mon Sep 17 00:00:00 2001 From: kaelthas Date: Sat, 26 Sep 2015 12:26:31 +0200 Subject: [PATCH 78/94] Bugfix: fixed a bug that caused subdivision surface modifier to crash if at least one not connected vertex was in the mesh. --- .../modifiers/SubdivisionSurfaceModifier.java | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/SubdivisionSurfaceModifier.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/SubdivisionSurfaceModifier.java index 2f3d12d60..2945b3413 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/SubdivisionSurfaceModifier.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/SubdivisionSurfaceModifier.java @@ -2,6 +2,7 @@ package com.jme3.scene.plugins.blender.modifiers; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; @@ -129,13 +130,18 @@ public class SubdivisionSurfaceModifier extends Modifier { for (int i = 0; i < temporalMesh.getVertexCount(); ++i) { // finding adjacent edges that were created by dividing original edges List adjacentOriginalEdges = new ArrayList(); - for (Edge edge : temporalMesh.getAdjacentEdges(i)) { - if (verticesOnOriginalEdges.contains(edge.getFirstIndex()) || verticesOnOriginalEdges.contains(edge.getSecondIndex())) { - adjacentOriginalEdges.add(edge); + Collection adjacentEdges = temporalMesh.getAdjacentEdges(i); + if(adjacentEdges != null) {// this can be null if a vertex with index 'i' is not connected to any face nor edge + for (Edge edge : temporalMesh.getAdjacentEdges(i)) { + if (verticesOnOriginalEdges.contains(edge.getFirstIndex()) || verticesOnOriginalEdges.contains(edge.getSecondIndex())) { + adjacentOriginalEdges.add(edge); + } } + + creasePoints.add(new CreasePoint(i, boundaryVertices.contains(i), adjacentOriginalEdges, temporalMesh)); + } else { + creasePoints.add(null);//the count of crease points must be equal to vertex count; otherwise we'll get IndexOutofBoundsException later } - - creasePoints.add(new CreasePoint(i, boundaryVertices.contains(i), adjacentOriginalEdges, temporalMesh)); } Vector3f[] averageVert = new Vector3f[temporalMesh.getVertexCount()]; @@ -174,23 +180,25 @@ public class SubdivisionSurfaceModifier extends Modifier { } for (int i = 0; i < averageVert.length; ++i) { - Vector3f v = temporalMesh.getVertices().get(i); - averageVert[i].divideLocal(averageCount[i]); - - // computing translation vector - Vector3f t = averageVert[i].subtract(v); - if (!boundaryVertices.contains(i)) { - t.multLocal(4 / (float) averageCount[i]); - } - - // moving the vertex - v.addLocal(t); + if(averageVert[i] != null && averageCount[i] > 0) { + Vector3f v = temporalMesh.getVertices().get(i); + averageVert[i].divideLocal(averageCount[i]); + + // computing translation vector + Vector3f t = averageVert[i].subtract(v); + if (!boundaryVertices.contains(i)) { + t.multLocal(4 / (float) averageCount[i]); + } - // applying crease weight if neccessary - CreasePoint creasePoint = creasePoints.get(i); - if (creasePoint.getTarget() != null && creasePoint.getWeight() != 0) { - t = creasePoint.getTarget().subtractLocal(v).multLocal(creasePoint.getWeight()); + // moving the vertex v.addLocal(t); + + // applying crease weight if neccessary + CreasePoint creasePoint = creasePoints.get(i); + if (creasePoint.getTarget() != null && creasePoint.getWeight() != 0) { + t = creasePoint.getTarget().subtractLocal(v).multLocal(creasePoint.getWeight()); + v.addLocal(t); + } } } } From 953a301d5e27a730449031e13174a30d21dfa74d Mon Sep 17 00:00:00 2001 From: kaelthas Date: Sat, 26 Sep 2015 12:30:26 +0200 Subject: [PATCH 79/94] Bugfix: fixed a bug that caused importer to crash when the author of the blend file assigned non existing UV coordinates group name to a mesh. --- .../scene/plugins/blender/materials/MaterialContext.java | 8 +++++--- .../scene/plugins/blender/textures/CombinedTexture.java | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java index 51c7072b1..3b5a8eefb 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java @@ -168,9 +168,11 @@ public final class MaterialContext implements Savable { this.setTexture(material, combinedTexture.getMappingType(), combinedTexture.getResultTexture()); List uvs = combinedTexture.getResultUVS(); - VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]); - uvCoordsBuffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(uvs.toArray(new Vector2f[uvs.size()]))); - geometry.getMesh().setBuffer(uvCoordsBuffer); + if(uvs != null && uvs.size() > 0) { + VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]); + uvCoordsBuffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(uvs.toArray(new Vector2f[uvs.size()]))); + geometry.getMesh().setBuffer(uvCoordsBuffer); + }//uvs might be null if the user assigned non existing UV coordinates group name to the mesh (this should be fixed in blender file) } else { LOGGER.log(Level.WARNING, "The texture could not be applied because JME only supports up to {0} different UV's.", TextureHelper.TEXCOORD_TYPES.length); } diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/CombinedTexture.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/CombinedTexture.java index b7d6958ca..e406447eb 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/CombinedTexture.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/CombinedTexture.java @@ -158,6 +158,9 @@ public class CombinedTexture { } else { resultUVS = userDefinedUVCoordinates.get(textureData.uvCoordinatesName); } + if(resultUVS == null && LOGGER.isLoggable(Level.WARNING)) { + LOGGER.warning("The texture " + textureData.texture.getName() + " has assigned non existing UV coordinates group: " + textureData.uvCoordinatesName + "."); + } masterUserUVSetName = textureData.uvCoordinatesName; } else { TemporalMesh temporalMesh = (TemporalMesh) blenderContext.getLoadedFeature(geometriesOMA, LoadedDataType.TEMPORAL_MESH); From 06d6f0861668ae77d2d802e97deb48f481d1fe84 Mon Sep 17 00:00:00 2001 From: Dokthar Date: Tue, 6 Oct 2015 19:54:40 +0200 Subject: [PATCH 80/94] GImpactCollisionShape : fix for #188, added a call to updateBound() in native jni binding, just after creating the shape, (native createShape() method) --- .../com_jme3_bullet_collision_shapes_GImpactCollisionShape.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/jme3-bullet-native/src/native/cpp/com_jme3_bullet_collision_shapes_GImpactCollisionShape.cpp b/jme3-bullet-native/src/native/cpp/com_jme3_bullet_collision_shapes_GImpactCollisionShape.cpp index d3e0ba719..f416e46b4 100644 --- a/jme3-bullet-native/src/native/cpp/com_jme3_bullet_collision_shapes_GImpactCollisionShape.cpp +++ b/jme3-bullet-native/src/native/cpp/com_jme3_bullet_collision_shapes_GImpactCollisionShape.cpp @@ -51,6 +51,7 @@ extern "C" { jmeClasses::initJavaClasses(env); btTriangleIndexVertexArray* array = reinterpret_cast(meshId); btGImpactMeshShape* shape = new btGImpactMeshShape(array); + shape->updateBound(); return reinterpret_cast(shape); } From cfdb9a87596cb5acd63570afffc0a824e0ba039b Mon Sep 17 00:00:00 2001 From: Dokthar Date: Wed, 7 Oct 2015 22:25:36 +0200 Subject: [PATCH 81/94] Lights (see #362) : added light v. sphere intersection, and implementations of intersectsSphere(), second attempt --- .../java/com/jme3/light/AmbientLight.java | 6 +++ .../com/jme3/light/DefaultLightFilter.java | 7 ++-- .../java/com/jme3/light/DirectionalLight.java | 6 +++ .../src/main/java/com/jme3/light/Light.java | 15 +++++++ .../main/java/com/jme3/light/PointLight.java | 12 ++++++ .../main/java/com/jme3/light/SpotLight.java | 42 ++++++++++++++++++- 6 files changed, 83 insertions(+), 5 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/light/AmbientLight.java b/jme3-core/src/main/java/com/jme3/light/AmbientLight.java index 58676851c..a2dc1cd38 100644 --- a/jme3-core/src/main/java/com/jme3/light/AmbientLight.java +++ b/jme3-core/src/main/java/com/jme3/light/AmbientLight.java @@ -32,6 +32,7 @@ package com.jme3.light; import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; @@ -62,6 +63,11 @@ public class AmbientLight extends Light { return true; } + @Override + public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) { + return true; + } + @Override public boolean intersectsFrustum(Camera camera, TempVars vars) { return true; diff --git a/jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java b/jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java index c8456529d..5d07e1a8d 100644 --- a/jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java +++ b/jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2014 jMonkeyEngine + * Copyright (c) 2009-2015 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -78,8 +78,9 @@ public final class DefaultLightFilter implements LightFilter { } } else if (bv instanceof BoundingSphere) { if (!Float.isInfinite( ((BoundingSphere)bv).getRadius() )) { - // Non-infinite bounding sphere... Not supported yet. - throw new UnsupportedOperationException("Only AABB supported for now"); + if (!light.intersectsSphere((BoundingSphere)bv, vars)) { + continue; + } } } diff --git a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java index b8e1a1979..b1abb65cf 100644 --- a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java +++ b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java @@ -32,6 +32,7 @@ package com.jme3.light; import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; @@ -116,6 +117,11 @@ public class DirectionalLight extends Light { return true; } + @Override + public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) { + return true; + } + @Override public boolean intersectsFrustum(Camera camera, TempVars vars) { return true; diff --git a/jme3-core/src/main/java/com/jme3/light/Light.java b/jme3-core/src/main/java/com/jme3/light/Light.java index bac775aeb..39a30980b 100644 --- a/jme3-core/src/main/java/com/jme3/light/Light.java +++ b/jme3-core/src/main/java/com/jme3/light/Light.java @@ -32,6 +32,7 @@ package com.jme3.light; import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; import com.jme3.export.*; import com.jme3.math.ColorRGBA; import com.jme3.renderer.Camera; @@ -196,6 +197,20 @@ public abstract class Light implements Savable, Cloneable { */ public abstract boolean intersectsBox(BoundingBox box, TempVars vars); + /** + * Determines if the light intersects with the given bounding sphere. + *

+ * For non-local lights, such as {@link DirectionalLight directional lights}, + * {@link AmbientLight ambient lights}, or {@link PointLight point lights} + * without influence radius, this method should always return true. + * + * @param sphere The sphere to check intersection against. + * @param vars TempVars in case it is needed. + * + * @return True if the light intersects the sphere, false otherwise. + */ + public abstract boolean intersectsSphere(BoundingSphere sphere, TempVars vars); + /** * Determines if the light intersects with the given camera frustum. * diff --git a/jme3-core/src/main/java/com/jme3/light/PointLight.java b/jme3-core/src/main/java/com/jme3/light/PointLight.java index 1f515e71c..1468d1ba1 100644 --- a/jme3-core/src/main/java/com/jme3/light/PointLight.java +++ b/jme3-core/src/main/java/com/jme3/light/PointLight.java @@ -32,12 +32,14 @@ package com.jme3.light; import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; import com.jme3.bounding.BoundingVolume; import com.jme3.bounding.Intersection; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; +import com.jme3.math.FastMath; import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; @@ -195,6 +197,16 @@ public class PointLight extends Light { } } + @Override + public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) { + if (this.radius == 0) { + return true; + } else { + // Sphere v. sphere collision + return sphere.getCenter().subtract(position).lengthSquared() < FastMath.sqr(radius + sphere.getRadius()); + } + } + @Override public boolean intersectsFrustum(Camera camera, TempVars vars) { if (this.radius == 0) { diff --git a/jme3-core/src/main/java/com/jme3/light/SpotLight.java b/jme3-core/src/main/java/com/jme3/light/SpotLight.java index de24280ab..444d58b3a 100644 --- a/jme3-core/src/main/java/com/jme3/light/SpotLight.java +++ b/jme3-core/src/main/java/com/jme3/light/SpotLight.java @@ -32,6 +32,7 @@ package com.jme3.light; import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; import com.jme3.bounding.BoundingVolume; import com.jme3.export.*; import com.jme3.math.ColorRGBA; @@ -225,12 +226,49 @@ public class SpotLight extends Light { return false; } + @Override + public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) { + if (this.spotRange > 0f) { + // Check spot range first. + // Sphere v. sphere collision + if (sphere.getCenter().subtract(position).lengthSquared() >= FastMath.sqr(spotRange + sphere.getRadius())) { + return false; + } + } + + float otherRadiusSquared = FastMath.sqr(sphere.getRadius()); + float otherRadius = sphere.getRadius(); + + // Check if sphere is within spot angle. + // Cone v. sphere collision. + Vector3f E = direction.mult(otherRadius * outerAngleSinRcp, vars.vect1); + Vector3f U = position.subtract(E, vars.vect2); + Vector3f D = sphere.getCenter().subtract(U, vars.vect3); + + float dsqr = D.dot(D); + float e = direction.dot(D); + + if (e > 0f && e * e >= dsqr * outerAngleCosSqr) { + D = sphere.getCenter().subtract(position, vars.vect3); + dsqr = D.dot(D); + e = -direction.dot(D); + + if (e > 0f && e * e >= dsqr * outerAngleSinSqr) { + return dsqr <= otherRadiusSquared; + } else { + return true; + } + } + + return false; + } + @Override public boolean intersectsFrustum(Camera cam, TempVars vars) { if (spotRange == 0) { // The algorithm below does not support infinite spot range. - return true; - } + return true; + } Vector3f farPoint = vars.vect1.set(position).addLocal(vars.vect2.set(direction).multLocal(spotRange)); for (int i = 5; i >= 0; i--) { //check origin against the plane From 7d07cedc4ceaec2b3761c9670d5d3fde11701356 Mon Sep 17 00:00:00 2001 From: MeFisto94 Date: Sun, 6 Sep 2015 20:19:04 +0200 Subject: [PATCH 82/94] Fixed Issue #46 : The MaterialViewer will now simply ignore not available textures instead of crashing --- .../gde/materials/multiview/widgets/TexturePanel.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sdk/jme3-materialeditor/src/com/jme3/gde/materials/multiview/widgets/TexturePanel.java b/sdk/jme3-materialeditor/src/com/jme3/gde/materials/multiview/widgets/TexturePanel.java index b414a1126..0129535e4 100644 --- a/sdk/jme3-materialeditor/src/com/jme3/gde/materials/multiview/widgets/TexturePanel.java +++ b/sdk/jme3-materialeditor/src/com/jme3/gde/materials/multiview/widgets/TexturePanel.java @@ -10,15 +10,19 @@ */ package com.jme3.gde.materials.multiview.widgets; +import com.jme3.asset.AssetNotFoundException; import com.jme3.gde.core.assets.ProjectAssetManager; import com.jme3.gde.core.properties.TexturePropertyEditor; import com.jme3.gde.core.properties.preview.DDSPreview; import com.jme3.gde.materials.MaterialProperty; +import com.jme3.gde.materials.multiview.MaterialEditorTopComponent; import com.jme3.texture.Texture; import java.awt.Component; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.Icon; import javax.swing.SwingUtilities; import jme3tools.converters.ImageToAwt; @@ -50,7 +54,7 @@ public class TexturePanel extends MaterialPropertyWidget { exec.execute(new Runnable() { public void run() { - + try{ Texture tex = manager.loadTexture(textureName); if (textureName.toLowerCase().endsWith(".dds")) { if (ddsPreview == null) { @@ -65,6 +69,9 @@ public class TexturePanel extends MaterialPropertyWidget { } }); } + } catch (AssetNotFoundException a) { + Logger.getLogger(MaterialEditorTopComponent.class.getName()).log(Level.WARNING, "Could not load texture {0}", textureName); + } } }); } From 505aa23048cdc8eeb83eadb454d224f484aac69d Mon Sep 17 00:00:00 2001 From: Dokthar Date: Wed, 14 Oct 2015 20:30:20 +0200 Subject: [PATCH 83/94] light : added unit tests for the new support of bounding spheres intersections (for lightFilter) --- .../main/java/com/jme3/light/SpotLight.java | 4 +- .../java/com/jme3/light/LightFilterTest.java | 91 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/light/SpotLight.java b/jme3-core/src/main/java/com/jme3/light/SpotLight.java index 444d58b3a..bd1c03c6e 100644 --- a/jme3-core/src/main/java/com/jme3/light/SpotLight.java +++ b/jme3-core/src/main/java/com/jme3/light/SpotLight.java @@ -267,8 +267,8 @@ public class SpotLight extends Light { public boolean intersectsFrustum(Camera cam, TempVars vars) { if (spotRange == 0) { // The algorithm below does not support infinite spot range. - return true; - } + return true; + } Vector3f farPoint = vars.vect1.set(position).addLocal(vars.vect2.set(direction).multLocal(spotRange)); for (int i = 5; i >= 0; i--) { //check origin against the plane diff --git a/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java b/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java index 0d1e71988..d4fc18db9 100644 --- a/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java +++ b/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java @@ -31,6 +31,7 @@ */ package com.jme3.light; +import com.jme3.bounding.BoundingSphere; import com.jme3.math.FastMath; import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; @@ -81,12 +82,20 @@ public class LightFilterTest { public void testAmbientFiltering() { geom.addLight(new AmbientLight()); checkFilteredLights(1); // Ambient lights must never be filtered + + // Test for bounding Sphere + geom.setModelBound(new BoundingSphere(0.5f, Vector3f.ZERO)); + checkFilteredLights(1); // Ambient lights must never be filtered } @Test public void testDirectionalFiltering() { geom.addLight(new DirectionalLight(Vector3f.UNIT_Y)); checkFilteredLights(1); // Directional lights must never be filtered + + // Test for bounding Sphere + geom.setModelBound(new BoundingSphere(0.5f, Vector3f.ZERO)); + checkFilteredLights(1); // Directional lights must never be filtered } @Test @@ -127,6 +136,44 @@ public class LightFilterTest { // Rotate the camera so it is up, light is outside frustum. cam.lookAtDirection(Vector3f.UNIT_Y, Vector3f.UNIT_Y); checkFilteredLights(0); + + // ================================== + // Tests for bounding Sphere + geom.setLocalTranslation(0, 0, 0); + + // Infinite point lights must never be filtered + pl.setRadius(0); + checkFilteredLights(1); + + pl.setRadius(1f); + geom.setModelBound(new BoundingSphere(1f, Vector3f.ZERO)); + + // Put the light at the very close to the geom, + // the very edge of the sphere touches the other bounding sphere + // Still not considered an intersection though. + pl.setPosition(new Vector3f(0, 0, -2f)); + checkFilteredLights(0); + + // And more close - now its an intersection. + pl.setPosition(new Vector3f(0, 0, 0f)); + checkFilteredLights(0); + + // In this case its an intersection for pointLight v. box + // But not for pointLight v. sphere + // Vector3f(0, 0.5f, 0.5f).normalize().mult(2) ~ >= (0.0, 1.4142135, 1.4142135) + //pl.setPosition(new Vector3f(0, 0.5f, 0.5f).normalizeLocal().multLocal(2 + FastMath.ZERO_TOLERANCE)); + pl.setPosition(new Vector3f(0f, 1.4142135f, 1.4142135f).multLocal(1+FastMath.ZERO_TOLERANCE)); + checkFilteredLights(0); + + // Make the distance a wee bit closer, now its an intersection + //pl.setPosition(new Vector3f(0, 0.5f, 0.5f).normalizeLocal().multLocal(2 - FastMath.ZERO_TOLERANCE)); + pl.setPosition(new Vector3f(0f, 1.4142135f, 1.4142135f).multLocal(1-FastMath.ZERO_TOLERANCE)); + checkFilteredLights(1); + + // it's a point light, also test for the other corner + pl.setPosition(new Vector3f(0f, -1.4142135f, -1.4142135f).multLocal(1-FastMath.ZERO_TOLERANCE)); + checkFilteredLights(0); + } @Test @@ -175,5 +222,49 @@ public class LightFilterTest { // now, the spot will touch the box. geom.setMesh(new Box(5, 1, 1)); checkFilteredLights(1); + + // ================================== + // Tests for bounding sphere, with a radius of 1f (in the box geom) + sl.setPosition(Vector3f.ZERO); + sl.setDirection(Vector3f.UNIT_Z); + geom.setLocalTranslation(Vector3f.ZERO); + geom.setModelBound(new BoundingSphere(1f, Vector3f.ZERO)); + + // Infinit spot lights are only filtered + // if the geometry is outside the infinite cone. + sl.setSpotRange(0); + checkFilteredLights(1); + + //the geommetry is outside the infinit cone (cone direction going away from the geom) + sl.setPosition(Vector3f.UNIT_Z.mult(1+FastMath.ZERO_TOLERANCE)); + checkFilteredLights(0); + + //place the spote ligth in the corner of the box geom, (in order to test bounding sphere) + sl.setDirection(new Vector3f(1, 1, 0).normalizeLocal()); + geom.setLocalTranslation(0, 0, 10); + sl.setPosition(sl.getDirection().mult(-2f).add(geom.getLocalTranslation())); + + // make it barely reach the sphere, incorect with a box + sl.setSpotRange(1f - FastMath.ZERO_TOLERANCE); + checkFilteredLights(0); + + // make it reach the sphere + sl.setSpotRange(1f + FastMath.ZERO_TOLERANCE); + checkFilteredLights(1); + + // extent the range + sl.setPosition(Vector3f.ZERO); + sl.setDirection(Vector3f.UNIT_Z); + sl.setSpotRange(20); + checkFilteredLights(1); + + // rotate the cone a bit so it no longer faces the geom + sl.setDirection(new Vector3f(0, 0.3f, 0.7f).normalizeLocal()); + checkFilteredLights(0); + + // Create sphere of size X=10 (double the radius) + // now, the spot will touch the sphere. + geom.setModelBound(new BoundingSphere(5f, Vector3f.ZERO)); + checkFilteredLights(1); } } From d48a1bb99838792285087fa71298801a9fc33c59 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 15 Oct 2015 09:46:35 +0100 Subject: [PATCH 84/94] Removed native library jemalloc.dll for LWJGL3 as this will not be needed until 3.0.0b and after. --- .../main/java/com/jme3/system/NativeLibraryLoader.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java index 53898883c..ab4f7a2c9 100644 --- a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java +++ b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java @@ -31,11 +31,7 @@ */ package com.jme3.system; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; @@ -148,8 +144,8 @@ public final class NativeLibraryLoader { registerNativeLibrary("lwjgl3", Platform.Linux64, "native/linux/liblwjgl.so"); registerNativeLibrary("lwjgl3", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); registerNativeLibrary("lwjgl3", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); - registerNativeLibrary("lwjgl3", Platform.Windows32, "native/windows/jemalloc32.dll"); - registerNativeLibrary("lwjgl3", Platform.Windows64, "native/windows/jemalloc.dll"); + //registerNativeLibrary("lwjgl3", Platform.Windows32, "native/windows/jemalloc32.dll"); // These are introduced in LWJGL 3.0.0b + //registerNativeLibrary("lwjgl3", Platform.Windows64, "native/windows/jemalloc.dll"); // These are introduced in LWJGL 3.0.0b // OpenAL for LWJGL 3.x // For OSX: Need to add lib prefix when extracting From 4be09e3505b3fd07e93d747ac872a44e7155b775 Mon Sep 17 00:00:00 2001 From: Dokthar Date: Thu, 15 Oct 2015 19:27:50 +0200 Subject: [PATCH 85/94] light : replaced duplicated code by methods from Intersection --- jme3-core/src/main/java/com/jme3/light/PointLight.java | 2 +- jme3-core/src/main/java/com/jme3/light/SpotLight.java | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/light/PointLight.java b/jme3-core/src/main/java/com/jme3/light/PointLight.java index 1468d1ba1..4b5224c30 100644 --- a/jme3-core/src/main/java/com/jme3/light/PointLight.java +++ b/jme3-core/src/main/java/com/jme3/light/PointLight.java @@ -203,7 +203,7 @@ public class PointLight extends Light { return true; } else { // Sphere v. sphere collision - return sphere.getCenter().subtract(position).lengthSquared() < FastMath.sqr(radius + sphere.getRadius()); + return Intersection.intersect(sphere, position, radius); } } diff --git a/jme3-core/src/main/java/com/jme3/light/SpotLight.java b/jme3-core/src/main/java/com/jme3/light/SpotLight.java index bd1c03c6e..bc1335b5b 100644 --- a/jme3-core/src/main/java/com/jme3/light/SpotLight.java +++ b/jme3-core/src/main/java/com/jme3/light/SpotLight.java @@ -34,6 +34,7 @@ package com.jme3.light; import com.jme3.bounding.BoundingBox; import com.jme3.bounding.BoundingSphere; import com.jme3.bounding.BoundingVolume; +import com.jme3.bounding.Intersection; import com.jme3.export.*; import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; @@ -189,9 +190,7 @@ public class SpotLight extends Light { if (this.spotRange > 0f) { // Check spot range first. // Sphere v. box collision - if (FastMath.abs(box.getCenter().x - position.x) >= spotRange + box.getXExtent() - || FastMath.abs(box.getCenter().y - position.y) >= spotRange + box.getYExtent() - || FastMath.abs(box.getCenter().z - position.z) >= spotRange + box.getZExtent()) { + if (!Intersection.intersect(box, position, spotRange)) { return false; } } @@ -231,7 +230,7 @@ public class SpotLight extends Light { if (this.spotRange > 0f) { // Check spot range first. // Sphere v. sphere collision - if (sphere.getCenter().subtract(position).lengthSquared() >= FastMath.sqr(spotRange + sphere.getRadius())) { + if (!Intersection.intersect(sphere, position, spotRange)) { return false; } } From 071ad5c6183f588bc8d1eeedd7ae97901c94938b Mon Sep 17 00:00:00 2001 From: Dokthar Date: Fri, 16 Oct 2015 20:17:57 +0200 Subject: [PATCH 86/94] light : fixed pointLight v. bounding sphere unit test --- .../java/com/jme3/light/LightFilterTest.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java b/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java index d4fc18db9..447682964 100644 --- a/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java +++ b/jme3-core/src/test/java/com/jme3/light/LightFilterTest.java @@ -139,25 +139,26 @@ public class LightFilterTest { // ================================== // Tests for bounding Sphere - geom.setLocalTranslation(0, 0, 0); - + geom.setModelBound(new BoundingSphere(1f, Vector3f.ZERO)); + geom.setLocalTranslation(0, 0, 2); + pl.setPosition(new Vector3f(0, 0, 2f)); + // Infinite point lights must never be filtered pl.setRadius(0); checkFilteredLights(1); - + pl.setRadius(1f); - geom.setModelBound(new BoundingSphere(1f, Vector3f.ZERO)); - // Put the light at the very close to the geom, // the very edge of the sphere touches the other bounding sphere // Still not considered an intersection though. - pl.setPosition(new Vector3f(0, 0, -2f)); + pl.setPosition(new Vector3f(0, 0, 0)); checkFilteredLights(0); // And more close - now its an intersection. - pl.setPosition(new Vector3f(0, 0, 0f)); - checkFilteredLights(0); - + pl.setPosition(new Vector3f(0, 0, 0f + FastMath.ZERO_TOLERANCE)); + checkFilteredLights(1); + + geom.setLocalTranslation(0, 0, 0); // In this case its an intersection for pointLight v. box // But not for pointLight v. sphere // Vector3f(0, 0.5f, 0.5f).normalize().mult(2) ~ >= (0.0, 1.4142135, 1.4142135) From 1bb3ee089a2102dfcf799137de69bb2b7a6b6d57 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Sat, 17 Oct 2015 22:34:22 -0400 Subject: [PATCH 87/94] Change duplicated docstring in FlyByCamera.unregisterInput --- jme3-core/src/main/java/com/jme3/input/FlyByCamera.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java b/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java index d1dbe0aef..dd9f00dc1 100644 --- a/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java +++ b/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java @@ -277,8 +277,7 @@ public class FlyByCamera implements AnalogListener, ActionListener { } /** - * Registers the FlyByCamera to receive input events from the provided - * Dispatcher. + * Unregisters the FlyByCamera from the event Dispatcher. */ public void unregisterInput(){ From 5e8f5e6a1f8f181b3d70a416a667659154091830 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Wed, 21 Oct 2015 17:05:16 +0100 Subject: [PATCH 88/94] Updated lwjgl3 module to use LWJGL 3.0.0b #35 which is the current stable build. --- .../com/jme3/system/NativeLibraryLoader.java | 33 +++++++++++++----- jme3-lwjgl3/build.gradle | 5 +-- jme3-lwjgl3/lib/lwjgl-3.0.0b-35-natives.jar | Bin 0 -> 2632785 bytes jme3-lwjgl3/lib/lwjgl-3.0.0b-35.jar | Bin 0 -> 1883204 bytes .../java/com/jme3/audio/lwjgl/LwjglALC.java | 9 ++++- .../com/jme3/system/lwjgl/LwjglContext.java | 11 +++--- 6 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 jme3-lwjgl3/lib/lwjgl-3.0.0b-35-natives.jar create mode 100644 jme3-lwjgl3/lib/lwjgl-3.0.0b-35.jar diff --git a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java index ab4f7a2c9..947129d30 100644 --- a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java +++ b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java @@ -144,8 +144,22 @@ public final class NativeLibraryLoader { registerNativeLibrary("lwjgl3", Platform.Linux64, "native/linux/liblwjgl.so"); registerNativeLibrary("lwjgl3", Platform.MacOSX32, "native/macosx/liblwjgl.dylib"); registerNativeLibrary("lwjgl3", Platform.MacOSX64, "native/macosx/liblwjgl.dylib"); - //registerNativeLibrary("lwjgl3", Platform.Windows32, "native/windows/jemalloc32.dll"); // These are introduced in LWJGL 3.0.0b - //registerNativeLibrary("lwjgl3", Platform.Windows64, "native/windows/jemalloc.dll"); // These are introduced in LWJGL 3.0.0b + + // GLFW for LWJGL 3.x + registerNativeLibrary("glfw-lwjgl3", Platform.Windows32, "native/windows/glfw32.dll"); + registerNativeLibrary("glfw-lwjgl3", Platform.Windows64, "native/windows/glfw.dll"); + registerNativeLibrary("glfw-lwjgl3", Platform.Linux32, "native/linux/libglfw32.so"); + registerNativeLibrary("glfw-lwjgl3", Platform.Linux64, "native/linux/libglfw.dll"); + registerNativeLibrary("glfw-lwjgl3", Platform.MacOSX32, "native/macosx/libglfw.dylib"); + registerNativeLibrary("glfw-lwjgl3", Platform.MacOSX64, "native/macosx/libglfw.dylib"); + + // jemalloc for LWJGL 3.x + registerNativeLibrary("jemalloc-lwjgl3", Platform.Windows32, "native/windows/jemalloc32.dll"); + registerNativeLibrary("jemalloc-lwjgl3", Platform.Windows64, "native/windows/jemalloc.dll"); + registerNativeLibrary("jemalloc-lwjgl3", Platform.Linux32, "native/linux/libjemalloc32.so"); + registerNativeLibrary("jemalloc-lwjgl3", Platform.Linux64, "native/linux/libjemalloc.so"); + registerNativeLibrary("jemalloc-lwjgl3", Platform.MacOSX32, "native/macosx/libjemalloc.dylib"); + registerNativeLibrary("jemalloc-lwjgl3", Platform.MacOSX64, "native/macosx/libjemalloc.dylib"); // OpenAL for LWJGL 3.x // For OSX: Need to add lib prefix when extracting @@ -475,7 +489,7 @@ public final class NativeLibraryLoader { if (url == null) { return; } - + String loadedAsFileName; if (library.getExtractedAsName() != null) { loadedAsFileName = library.getExtractedAsName(); @@ -522,7 +536,7 @@ public final class NativeLibraryLoader { throw new UnsupportedOperationException("JVM is running under " + "reduced permissions. Cannot load native libraries."); } - + Platform platform = JmeSystem.getPlatform(); NativeLibrary library = nativeLibraryMap.get(new NativeLibrary.Key(name, platform)); @@ -540,27 +554,28 @@ public final class NativeLibraryLoader { } } - String pathInJar = library.getPathInNativesJar(); - + final String pathInJar = library.getPathInNativesJar(); + if (pathInJar == null) { // This platform does not require the native library to be loaded. return; } - String fileNameInJar; + final String fileNameInJar; + if (pathInJar.contains("/")) { fileNameInJar = pathInJar.substring(pathInJar.lastIndexOf("/") + 1); } else { fileNameInJar = pathInJar; } - + URL url = Thread.currentThread().getContextClassLoader().getResource(pathInJar); if (url == null) { // Try the root of the classpath as well. url = Thread.currentThread().getContextClassLoader().getResource(fileNameInJar); } - + if (url == null) { // Attempt to load it as a system library. String unmappedName = unmapLibraryName(fileNameInJar); diff --git a/jme3-lwjgl3/build.gradle b/jme3-lwjgl3/build.gradle index e81175700..3ee3b0f65 100644 --- a/jme3-lwjgl3/build.gradle +++ b/jme3-lwjgl3/build.gradle @@ -11,8 +11,5 @@ repositories { dependencies { compile project(':jme3-core') compile project(':jme3-desktop') - compile 'org.lwjgl:lwjgl:3.0.0a' - compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0a', classifier: 'natives-windows' - compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0a', classifier: 'natives-linux' - compile group: 'org.lwjgl', name: 'lwjgl-platform', version: '3.0.0a', classifier: 'natives-osx' + compile files('lib/lwjgl-3.0.0b-35.jar', 'lib/lwjgl-3.0.0b-35-natives.jar') } diff --git a/jme3-lwjgl3/lib/lwjgl-3.0.0b-35-natives.jar b/jme3-lwjgl3/lib/lwjgl-3.0.0b-35-natives.jar new file mode 100644 index 0000000000000000000000000000000000000000..79b12bc5d22c2ff860472581f678a6efd9d67b3c GIT binary patch literal 2632785 zcmZ6Pbxa*k+^<{QrBK}6-Q6i}#ogWA-HH}>io3fPIJmpp!JUI2^!E4W=Do??KXy0S zPiCHZzLU+&D9b@YVSf4VY7AAE{-2lsdxQP=bTD?a_AqBw{@>8R{})33e<3?-2Y0Xk zjlz9-iRF+s&HrVyn)&66-rAQh*#Cdj&f3J%&cc())p0V%QUqfHD=2vu0_vNOx`>KM z#V<@rQa8*nSUMV;C<;vYuxv4v6ecpKxbME+D<1v^qRM{}LR2xK8-k;ZI_N?MmZ&9v zZre2mlXlj<`Bp_X-A!F&X0lsac!;O{L^V``AWFa>R&9soq4<8pEZg;`dz=tVYk|Up zYaGv|ij`wZ9C<#%;e$ga9JVqBLAs5hLbhcU(n?kMQ28faMdDYGfn}6|B@Kgc?SjZx zEsVJd$d3co;$R{^Mp@LUA*I=)zcke)bLSs0waG*8Pm%4t*;dqq!JR?Pu!mD3LPf`5 zk%PHARFUprOz~EOq8BBSV3Fw}Yc&$bpv5n1hzA&nZ6dOBZwt!!-+~FgB%vJ8CuUk1 zkSC^#*aS~XYeS&H-oHVkS(4ObD*jWIL{P+|=u}N4D+u#zgD4PVl#nD(1S!Z4 zIwZ^oI+0qWbw5BL0D2&Feq4m;OTpL7$i^y>`?)($k(Z$Mui9VrJrPG)NxFh!Aw)|7 z1SIUUZ_*+W1-I)WM35Cw17Cm%w>v?85;NjKZ;+#&96iXLAJ^qVzs{P<4ImCk65mBM zAiyvOyooj>kh2dfLHGL~eJIEzW}AV5<&X_fCULLVD1}b`Z#(fF1MXgLv!T<1f9@(~ zJLuhnPIp!fkjyxJuENf4@>7x_3&9w*P!w>2+>mUzqjV%kG>8%7It4x*VR7N#tO;XV zYehW~g_@xyAfqJ~49epnfiZby5MwFz?D$IzBwL8-*{PBr+mwLCV2;Fsfft5x*qC7858$LhwR{p`> zkx&QgNoO)uCI^>)9fJnKQa~Jfs!YK&qWnNe^dM=V351sTE{Of7^Xryp`d4141EEAO z5_ytvtIQaY##vXUL~@aVp!>oLg2X`JN0&H)w8S;{aDv~r`C8(HPz2%Q>uBNQCYB4F zee~KOS8dA2gcBU)4{xwjNV%U#3e9EItqCpqHMYa^zHfFkaORQ$u*=VS)wdRNCDY;%tQM=4VjeU@z2VNb2oV?Vrzd`1@Far5GgHwh8plYrX} zpy&?WRPUE_;jOhC^-B|S(e@I~CAG=3F#aIw)2xs4#4K-os~XB+FCw(I=O> zY^)D24oHtYkNMmF2U1ZkEzYI&aD9Gt&@kjnjxl*FdHnG}^uzWbj;C6z54py{s!YYU{Mi|&k%3+0&{cbQki94EqwSA{bjY2 zO5nxmL;x9UhdT&uD!8dA^$yPzwr>;4hS#=3J)F%X#qY~< zG^7&iWxx~~|N0nxjv5~{{M{A!J|k(pIP>j15S=d`lOw=Ir59mv#s#MvyC}C}Uii4r zBsS*j`Ul;8gk>F%1M6E8jA`K5$PGK;L#XjGzO|&pzRirKh^R0Gt5D2m1g<;WI zAJv-uoF^p$Z3;h-CeTVMfUoMHmu`FD9U0NS5DSs%)(6IU0Ax{La{V$lsJ#eP`10gq zVC3eQskv{tIvP|L{@tt4=UnE1j4XhEU-M=#exn^t0O&G$32Z!%S=~KF|Ntw zLF_#ze~iEwrhXHwZ%*js+nwft(~yAkCJ6x=ql;^PmUVAPaz!kYXGXZ5o!Vm@1wSIu z_l4BZT2J7yJJRbX;06_b@H0@h=zWU|)O(L0)lE2b!!G@%m&W<{MlMjv3KW$TxX!>h zXB@dn;D-?oV(m2mK2X3AYJ&XXq+UZ=-#VZN9UpN#nCOis5s|LKK{?+;=62j9kv@%9 z+t7CU%%h&H=Y9tbt1q2J=hJ2x-M0~u*ESB#^pz9hHU!RskYjS5ac>RWJ{6Kz1M`jJ z_;q?^O$6mJ1M^5&mBd-z)I$0(SYOWL&^1+Y0^k5Qfe0FXHz)1Y-%3j6z_)ON(_*n+ zMWz?m3`qRLIL^~(b%W?9?>*113bF1+^by3{rq5Gf22t?=uepF@VPl+!;XjAjlzE1F zDn#}Ez<2L*Ogu+cjER-8LT6nb(1#zBl)%3tC@8)OP&Nb{8Q*Kd4hq|AJ;CGbzv7yH zz9@8~AX04~>j7};0!{AaP{{)-l>2Lr;rdgyXi^AK@9a0h%0hPCRRd18+V z7iaWOG4?<6ybR?0wpA(q{TiMIadAF&V)g>rK|V|kLgz-!#L`8Q!;Sk^dPKQ{MRQKg zC+${5@mOCm*D6v?R=hTA0dZqOFBlvYPi?Lt0&V=Pj_0iVH+Z zCz#IiBAtYg7(W2H60~CrT?2+isyZ;hVN@|q0am9o>;j2AiIWHj$MxC_GH27qBT7by zYh!u_ZeH?Vq7Z2yhBC4M2eND`_~+ciR06=Y1dpRX;7kfsU&F8VW1xZ!I%s#&i^BeuPV$PGK4#j`j|7?ayT()o zvpk}IDExVZeSfp41H{0`0*Kbf!+uB12XhPMVuT`s-0C4Q;N{7`WaJz)=p$i&!=aHW z+|T&S?k2;K8Lxdu{?*o%r`!}CKgTIKm|J2a+==zJXk;XG39dPO5K2M%NvXE3p5F9Q zgqx1yGQ7I>j~zjj9alca^Z^-WhYkceCwUB0@(9u~DL2ATp#;LwSlDgp#vnpvZXcLs zk~!$8TzL3;8@7E3N`HVt<}MmB6K4)~?q7+Gqv5@9L`}T89H`V0N;p{MMe^PAlPD`A zF%A}?05QwwXU3wY=N}~lJg`6A*ti^i$jyGitsACh3W=i#m5^~7{Dw|Q>k-lZMQ1yI zCgwN>`!YI|)&<{FO!Ppup~U&t^{MDd&WWS{oD~RLEg+M>XOh{l2O0xrg(LNH{Q&CF zx!96W><6Hg4AHAZ#UZDPCct8#lL5^c013H&q28kmB5Q+2MTS^sPw{^w9K}0>2vHmJ zNCq(_6JpF@g5Gn+_JSYdy(y#eNOJPOoi=bfzkOrTgWqL{8iUw_hd^K*M@j%iqp2$u zvGV5da3N37jk3(33QK*5N!_5f8qFVda@GYwON8t5w~>STi6CN&X%&J>XBj!h^l zm`nCKF{oKo3rQ9Xw?S?I6#q*`5(^)};xzms%o;>>@pr%$P-cK3PV+?~%V|#+i6Al} z$C3k4qLPIaPk*n1KR0p`!UzUWjH&)8A`2m8S~3nABRip)opm#PDt>Py#sQ3m%^YX> zGS)6dJ(+aiRcbX}7%U{jIAYoDMM_A0QZCUn z=*_T)f!laRqD%FRJ|=Gxv0u2qD!`I7aIBWyfSKF>hjQL~Pvu946PH$`h`A93??4PX zVSM%GE>y#~M5|@785GVWD}IjFZ}?&>FC=6ra?}ox6eoYkR~@_=42Ruj91Au^y^~n@ zsNj1yf?%uNVfMlPJx^ng%33vOI1fHoW<@Y7!pHT_QhZ77*>6jJcj^kP1GLSbj9*Apz!_y@aX~T7x z=>$zQf*M6BC*#gBR=&lv^I;&3{u*p+4nwKUBY5xq74+GZ3$3_O(#Uo4cwHb*rDtBB*i^#4fD4>D^d23J^21r5?|XTqpAulx8eS=w{LkpV zm}-2+g!>2?-0L0rUuL}*VQ+=Rwdr|@fp?L96(B#O2}LVIw-X6zdeR-E4`_1%!s2cS z2f?i6Z$}u;yjD7dGNE}6;tTw3muZs29>PgPRS6`uj&Fhlpr`K;5?N&ZukVtbJbq-T zQCKC}lwNq_HAN%OVfX0G-PM36kdmk$;TxC^a6OPOC;kTg=1U*-2_1;S0JLI=B^F0Z z&$>ZOa%Ms{CIgu7{#z30(d|f+IC&UX55Hq9)+-gRMiWKWc0Me5hpn}xV93!4>qT0m#kO5u)M$}W{9`$RVJfCN$eA~}f8*iqDf zL8zbyr#Eo{=zo}wLHoo&d{QFle?TMR0+9dUWc{*KZv-p^he6s){Ldb7*gt!yqMK;INKyv`5II^~#Pbg6)fdnX$rS|1^dH+B55Xsz zF#JDrO8x(E7tj(yo`p$>P(kcyN+Hk0XiA~a+W%8t&rxVJVOY?A)s5dnk4hJN!vxHT zcjExDNesS%T%t5ZyCHxNXnk-XizvfjFM2?oST`bYh4cdp$W5}13d;V^9wQ3TsIvj= zzbSZ5su=7A31}e=K>TOVG3?*;xc<+c0-9&gzv-b*5`g+oTHAjLU;o4W`VTV@t_vBM zK>81^>>r#LEMS3T8y+-;b{*3G1vo?63kzf>4S)g${)cAxADZKTXb%6O!Tv+L4*%Ei zj(GQXpg-F6H`i#-%bi~x2Y1W>VNpLgV7~J#y5KY{iG0{I34rz=>_2mTV*kwTko+^J zLhy+GEdd+;-!4T|WS!T9vmT?x_WXXHUt9 zqgsz14KmmdkI~7HO7X$RK#YKWYYcqwIZ@Hoi`Qnv&)ILp5KIouevAA~>Cb6FYCIy{ zqSar)u9!%%W~IN!g}bB4@!Lm}i$wm)kNhsw3^e-<|IC~WKbAI@MqIKjL;eyziJ>jg zHo8Sd)~WN61W_o5sg#WN?pfkL9<{BwEhg#~G0vwh;kvk7N$wLjjG!O^Ys0uG)m$~L zr!O%xpIwk}LYwSHO|G*f(m%UQx0YmdZ$6J%CRR3(ptgfc{xZ}|y@0-D;xd6bw`7rS zLDFePxn+`1S=gp)-yr1{G1aLZbu?*U8*{?xs+ zng^^E#CyzkR;jhZf>1=CDai};oeWf=E}5ewX(!*yypBXVAjg9A<+|Y&h}M(aLrFA; z+gH6$k~OG5A%2hd@e4`*G$B|r+Hu*`0DE6Qr=XL$392*!NhN$=W+ zh41jlq-WH$&+m7rxojO52T~tb_bW;XN7^y|xRbX&>+xwlKOodNV>%x$8u%!f;%bq_T>IbO*B%s9y%^?MY0?WHTQYMfTj89aUBNT55veil)l0Ho-94==V&C74>Pkfj$*uhvSah z7UJ6-fr_}QiWJqA&d-&P4cnGCmz8t1R8{xxGqnw)E0M)!RXe=C`-4}=pp}_Co#W!U zHMRmJ7^Cw1d zQ;swmAa!{6qLZfk6!sgxL7n`fe&<8lQDLpRI0C5$ym`;E^)HCcQ`n-41Az{R{*#lH zxk=_{z#>?8eeIA#!=RpncOtg=Rz6So_ghuS{N;+0G(#i)X!c?0DS8~dbX|P26dY#b zZTBKZ3gb$crR+W37+molr=`(_!Gk-xUF$voz6>dT(*tYAHves#~C5d1zg=%c+lXgU9A{*unV^zS#d%Hv)Rr(0fwf`cm# z=`;KmjVkUot-z7O8cp0-Q6S&M#=KxA<#__Jti0Sij@d)_joE^tp%UJyBNw_m;KL0U$d1sDokDy)$ycyGSl<-5XhF7gH(DGVxEc7Yec5#71S!Dz@N4%b z&4oM|qsREqu_i{ZhA0p0UF-XIaF2&gC>!L#ahts|1t|!wkiZu>_m+k&# z?-mtO#}+>ie(&Q40WR~iSbIsr9d>Tjo6ILR>oYvrv%{i`cq26>b6+Jj&ylk@wkwOJ zXJ1(k+ZyT(S;5$v76A`J!b8DKeQr8owdAag3q_{NUIlHc8UWfGXm?%%;kVW@*3(vL zcouj`@F^UR_AYyVsSaN{ihZ93;Tm+!CjFyoX#=E$#4cm*yl!UyEC(6w0z4Ezi=W>dYxd_)edj% z?c5}B`|eJuZo`?XF*tpBT_ubz7nq?=?b@cK-ANjJd9lX>P|>=Le~lFQT)ZEtYgv|m z*T6W)<#)NWR_T2Di1T)skH>wWq&@qdY%{^HDsFp=g-+PUL1+JLX3VZkYdJ^0|Gm=j zW{mpD);d%4er{&aCAEXw-_>y9KGm?t>vCY+FC1?hX^Hz_ZgG|+q)ydV7gdSN{=J;^ zv;RwN^3MH7rG>ozWNzmOY1^mK&3AG>;wyE<^XU^tR~iz*fFJ823CVS%v{Tf&`ct#m z5<7k`!~C%-%Hf7beJQ3m<6Nd=4rhU!)y2fk%D<17S*(&aaL;mxD;0M0oopBHJzrr< zs)ai#pUbOc>yF!$bD0a)LEMW!QA7EUTjFRMM$L_P-x_)an1%T^sdb~MeGcaKkr=~zbnrMT+p^YnSs`8pS#?HNAf z(_9Gs(?}VgWGGeGe>8CuNUs^Pw|h|(ERtD{K7Gq?>@ednJg!X|_kg4q-O6*F))0zU z+7P=%9JI@TXi@+}_ayv?4xcC8QwUU`-ND}3rPtiFAaRIa&1%+=&1@QFvEEti|o zUE6sfcm87#!MAxR>z;`gE1xJPB$tE}<|Gm87$k1VSGjTrW1xRoJQl(+uGt(;jUA9+pd^N9&3bdMqzjG14V~zx%!__Yp=n@Q3Wv}yb<1V zvCP^Nwer=J9H}-ZzWVRBRY_L}^Dme-FH67{DinuwGBWGuH>2B4Xw19s%xz$+_7R2T zysFbneVHltj`J$v1(%`pU7F-p2~XH z2`l!~X1Xje$4maopXTq!cqKCFFrM#1&%T;}L3t8)W7+X_E_{_AP808rRCT3}{F1wM zg5k%|99PB z?_1dd#P>PH=*4GBI7_J-w$1iYG_7ZVWO6cMO~yd5sKLT$*AITi)4W@(*N^*4J~?8q zvZvO@nJiCCOLc{HTIO`e3#XRX+8#%Tv|-%5-icYSiE{K4RPcl<>y$ zRbobZ0r}~pVx<0hflS-hldopU>EwqCT>nT)4?RuKAD`<8Q-yw%3FNYL)IjPpo2F-3 zbQN+o{K2KRnm+CqJ!O2m*#;TuYx$WLv6m;C!pDZcvYLN zp~(iqN{f6FAG$5S6{Ahsg1b{XrXTxwUp8p1*$!n=In9up(wZk`XxXr*es-)!w^kR7 zMx(T9PuxWbI?WLkLf9w)NZjEJ_bGMTn?;C_?A#oT%`(_%-DYQ zbxfB7;yz`@(|dvapWK z9prCE3<(MC292&mZNroihk2I7$)0A?JxZ@Vnco_(pCB1wv0*g$(jx`#{3MdF>Q7yw z)i#TzkUx9ku`8qN>`D59KWY?ZanlCXIGH05!0j>#8`}h?frxm2COj$ZA+sK#8 zO_mE${fyru_m>m2G?Je%1hSe;+*2GnG!q&P)6KPMedbEDTLUQ6LVAP^df)fRc>xo; zJ#i<*e{dNHT>@S9h#ORGd}MV5p(Eq?k-GRHk&>`y&4vO_| z*bS}uyf=2bEVM}pd2XM(uv?m^idE+5XXqt(Ij^*Qeg(^wJ+@Z*A_Xr(Y&kBl_f2KW zukTlLo%%IDzP-ZQJ>hnkUv&G3>DJ2?0nec80Q%}EuWH%ePcm;oqms~7I-0U`Gf5g_ z#K$QFEQbC;t%I8f9^$K5J}+4jag?y%2KTwTR80AsrBaa)*re$D;*Y?QNXIx#6TMs8 zXI|&OoD;$Yg}AkRZG8>4x~tgzI2R&AEoZC|4S(i+NP_Z?YIIYV(&c8{@VS@N`JuYp z%%$}WH+vL{Mh>{@B2~5AO+G5qcQoDFpoCIwfPFLRCHIx7jQ;w)yrYK_hV3TV{ChTD zHM%_U&(2FN1N*j45a4$=0_SY-;>Lq9Z!>b0Aa=@utES|NPfhWqV203>N^6>4|L?Ge z%X}Nn0Nu72GGF6_w2r2!mn5mX0m@+tgU_Y@U`6-u+>V5Zsv`4?fG}L&d+HLsK+BuF zsn%61g>G&RYbpP$p7Slt)Ac0Owkv;9^^y2ewLyb8JiMfu)Q?;F0#qob2Fv0lgQ}x7 z!nW=E4gApMiON9bZts{QJF=FBMK#Vs=#@JA+}y@_#w3m!S1CHvk}cgqH_wj!#@b~W z&+oc$R0wxD(Axn z5LLJ$pp}`4Da{_LXpVV1rnHOc@uN{tv!0w}7twaSN|TgyaC!)8Bt}N&^KILRhtDpT zS61;S$4_RJ?!b1R*@oDgJq;Xtl(^s59#K;{B`G^@cgqg%zSv}OE^xCU0e{vNLjqE&1fAYmN0N6BOTq0<-6o>%0a3yV)h zcSS|NP0-&*za>8RWEk*^1a#(M$qa^B&a{$32C1o^LFdbii^43jBqP#D;2SZ^=6_ zDU8EvDJYgMFt{t7I&@atnOv}{_FyAXf68w!ZFY#l?i^{z-#ONBU#JrX7&MI7*;@I% z9t7fy(<(l-)>bFOTP}5KIp)1coo74B#^|#v9j*_o&K>m!mfO(@FYnuX7-i2WoBI7y z@IBvq!p|LFkG|XHv-YK*ac%cK`neReWmA<#giOChcR0!7VdprX_%VIkbp7mzf$Ki* z!RJ`J$IW+IDvRo&p;mK~`Ez83wvTF~dyC^bpS@@dboAnAdCe7Dj~K6CH|LNtNZ#oc z>E}5?(L&&s=n6tXYp0#b#>XldMC&dM3zOt*(lgsX9=n{l^2)4nAKL?z;fsBKoX&R^58#F03^TQr>Bw3|=OPpy;8nTjb=*QRY>u~Dnj zt0z6^ji=7h108cfKCyty0Z&)#>Cc%RWwG^)kMf<;ocIL>#9a05hsGXqF9K!NacSx}9fN9jJAI?0?p~o3hqFB5T(8 z5BayYh~mE&K{7J)Y9HisM_=>E={0WOnH@}R5XCSd?MCdD;x#ix1QWF%K&HOy#hVE|_p1D;*cKz`_*d&Kg?Rk`6Vb8WgHO7dl zdv$Zr(X4x#{#x7n^roLb(q^6IJ)@rm-&?)AGk?=x?|HW%W!pjGV&R~%`SNRfo)B+K zmOPZZBH4E8h~=U;{=Stv9J#y9VD$1N(-!!fb3HxjjW+Xa^_>L7$l3M|&DrMbBy@fl z8T`b()k6Hn$C+?^Lt|yWL%6&sNU^3krQ)q*B9wig{-+j)z-B&xOx_{0Q+I(SGIdRn zaFGo^Z~MbN5pVAK(prB^^Sn&m5LPoUcgJ>%9H;qxWMl8oN15f5;>%6YwaGnrHh-|k z5k`<8e-jk2yjaiO++}^!@>JG1-Gt={SB9k~l%-F-L+2Z40oI>ik=BglB%Y$UNMdOO_ycXD$#Q3kkZ&9&!3rNQkP=kh<~V!p=2T6Dd@SEI9ayf3HLggff7 z>g;+Ca+moz6;pbBi#fApt4qw$lf_u0zs6Tp3uyS5iqW>TSeoke>+6YsxR~@DSRpD? zqz_hirsnhn2x_vwzCBJ4*3I|YY@b}@_`7)Y!c?depS-(&te1KD)+|qNwD&)8d~PB? zR7LOE{5ouFu6+qO>pS4^|GjYi==Gj~{QYhAk+D|_PrhO$GhjVCup!Uxy>0C7Ai^(R z;3A(wcft6EZOB?>M`UAH98y2mu4_X!l(SFJ%6A@*vC89O)&H(GncG3>u4$RO!|&-; zughkXWdlcn8B3u zcR_+zjHV+qLLLpiH;d^#UjAPM{K6&4vR>{)+KkTkFB>xz@&22RN@ARX@D(GvuX($J zOYE&gbL&F2z7+Gk80X=I5Lq88(FG& z&D%n1^fCJR3+DIcQ`DJlyhHSC-P;_DvY|v z$=UNIwz*@|U3u4cB}n{z<36>ob8CAdgIO%pk%5!O=R63lD64G&*6P#iTzAe22VJ9( z$5>}8FZXoP zIXS$(N*bp<56F&{*i-(iEnOYp+iYrhJIOPT+6!6>1)yX2=7fDW;cr7LSc#SIS~yxwzp^i-U2q)Yeq)RtTh ziw^>8)xHH8o@PT0Xxh|UJ6Uvfd7iv2OUiphGkXSWLFd3<=5o&Pj^D@mxtE`Puz$cF zRW?@~*kmo)uUqLnU-p!t5;=R#?IF{UIVb9s z(B5RAzh&2aWm|VVjPCx++uwW98v9H4|=}#O)?Ux=v1?eO};~ieKiK)xhhL{M2om4MB!F!icJvEAJ(2 zc3Y$KU-t&K0|*oP-sxhWr!K<@-QgL;^<38u?8;JyE{T&Xh!hWWEmiZ~V&{i= z`H$EN=XhF=EeYL)zq&iM=~RReKGOt>EN=TB5dkjlnbO11s;_a6sbz_26Ps0|q~Xo^ zQ?@DA44q`@G&Lq4+saRMl~H<6)Z7cwCIJsuJdykP#41<2jjpH-wDao`kEIcK;1YZ> zJjWfl`TnqD(ifQ^3evWiYpXjV$ zk?(N{{q_@Bf4MjneK8*4k!p1%kXI-*BH7Z<{|v4zaIaNqX>9d`X>H`N)fc}}ozM(R zv*=uTW^_qSvoGIKk^M=g(!4}Spw#F5*Rr0hjXrV+o0q@!^X}9$H|~mlLHaQFyWotS zUU?YzBwUxn8N((v&x{(?yfRnH@9r_08B?60{? zq2SoM$jG`yVXO;B6YnG@;or5iHk>xp1gZXK=xzn2DuzRwA}zgcA9PBbnHMB=qu|V* zINI>}_Ee&=CNY~lRNgYL9F;7}q2Gg<1VYCe%Z(G^%Ca6=r>9n0t3L{2o3}2DJ%8%_ zJci-XLPtHie$sj$Gk>B7Z`cnBwv2DOrj9k&Xj-0?4#DiJwQdMmE1ZA{t3 z2=ja}PD{wYpIga)hDt=Z+`UPT`(>V<&tS&!>X!$`&UQ#Lc^h0ggOpj^$SQRnA` z;lCA)K8%4+6Oq@9!*A+hx$D{efY*JBcb~RJk(TDR%HG0AU@9eq!BNcf5B>+?Wfn_; z8zD>A{7#pteQMaHo0>?7afI6kkN1{`W?1mdqFQ^g~$Fmz{}Jib{4pHMX6S$Dy>w_r59sW%&>4DyUEPYq6xM{9n0Tksn+S z-0On|bvt~*7Kdg8c^WG6er_5X{vfjL=&AV4_anRTw~OFl>twhroiZ~A4uMZ>NlPNs z@VjA9&*i%LN>6R|X2-I^Xss*vdHKPpi6oiWx+KlH&~k%Hd03$;uf}2TwfX^_bQlGC z0iwTJK)~J1WVhiR_>K3@C(rwcGo#t(+IPk`yefBh+#=W!iZxJ1{N^yI&^%C{hRa1y z#{^Tl(1tu!AteG<6=XLE!Lsj5S^(%{zMe~^@+vo`Al<9npPhy`PNGkl2V2>r0Dnk7 zfgng5UH)#8m#L>mwO1WQCfJkHf8*zxwMQh`1V5eZ8c-*huMZuSb;r8wc0pGJR7r)D zry7dre@}rK1doRr$RB~}&piewvZk)LZPfth!zDW3{atKB?+Bzu@93WKN^?c&RFRBx zhJO&H^2b=?#kO9! z?KNA5Zo(P7;neKn0S8deHp2E%cJecLLa z$HC$GL0yAaMyHXw2b^!e|N5kjzOoShD{TW=l zV({pC$~tt$r6j6eGNDhl(B)@D=(YUR5x^BkBsZvGu1LS!kE&uLQpL;&n#?A`&R}|X z4pnv_f7Pg^g17#JCJeFViwf7T5X}12#~zSmuGt=?@0R6W|K!tvC&5w30|iVS`rEFa z?L7P4CBjP3@$p}Ht56PEIVrks-boi_idt1$x?3le(=M&cm^@txnF#uxzvO=Qo4DGe zQr4mXZ$p8r?eC)I(4^Xwhy7pWuD%cnms6u*Sy(++HqlWBHd)oF+?_`mCS$sc_Vy#F z^`qOQsnt|ua6>BPV8(5Ews+S0Nq*p_B3tP!Dbsiiubq@M>{&V&m5h3zCgK(g*%!iF z!8{Ou`^4|J$xsb($FuSKAvmnuk#1qeU5uWkQO#-5I-I`s+q+de=vG7d4KGoGNtEP! zt743CdnSaco;+*mgV`kqirKfjk^)SW3NTViN2b6E(=&>5dDoRgWoC?{@E(KwYDcf2 zS_k@{A+lXn?zV?3O#>KWFcKojlEZR4onKC5I9_Dix0p#OXy^8MCO=Rv+pf|k0CoGm z>7Z%t+a_cmCQe{LgAa0d>9Z_>xWvVB9VhfFYE*oP=66_0E5yXoxD(^6^6u5+2MAX;qQMl{GMHrUkF{x#qFQn{)*k-STsG=0_hwOrv<(D zmAsnxKb08FVCj2X;lQCa9}8U9^^ad~SB$PsVRqPVy{87e+mj{%sBRZSH=PryH>2@w z+x?c}5Zpb25T)IBZbEI#IFl?$2BrbV-_H%Cky;h&YQ{e3lXPA~5c6k{!7$vRbu|VC zLC=ohIQrATJTo45>)uO4QLp%tDQod%f1er`_A0<5l@EubIBW_hbcx0jc=Xk}HM8`{ z_x1IC(z3A)^Z1L{tjWgP`kJfDAEUfjf6+UG*QQ^0WdJ@p#6Yfy9D``4n1J1(iQk51 z&Q_!*yA4aR<~~3*Fk)FI@h=^{nn_1&2&ctvJ?L*3e3D&e-Xwsh=A-v%X!&EZ zP`FO}f1pYmtO55ULcU9|jz z@*UD&6#0);D2}U$eBLbcUr^yE4Z!$4Uk%wu%5Sz2$ePwk`o-~mIE)4|G;W#LFMgHv zQjq?Rh1LW&Dk%W=4O%7+v}lsq@90SCJ6+B^4NVE~jZLgP9jfm#cbNX~MjI$)e zPrCScrz`ESk5pbLIlZF&?fK7wGJ0T(^C70#dv%_^$F8s2v zFF1vXc@$KOYD`~bA>1=BrZSLyPhKeX4tpANtQ%}Qn^KZeoLaUlGe_<2V4O~5u~~eX zYT~M80eL=0y~BZPK3QHZqK&8&zajLZTIkJIUuyGIHAC{R4F&sl9LEqKzkl0*%zb2z zWHXsDm8vr^y~saCqx5Z;afG-+b3& zIXBenPG~Bw!k(EXtO35m6`VKCoNxH*2<5oJXqdO|T(nhz<;H94M(vfm<;;E1Xrw#1 z$3MYOV1y&Bxar%Py8j$-R*_$>PS(Yt=kkz|5Z<=J$H!CutOaW9zh9d%PX8Iwn{U8J zM6kYla&yD&OWtRx_cga?ND|_dGlq6vFWrjN^(NH&KUb zgLnW5bei75{<4V-WLGCPmyCYP7b=`E7t($3K_druIfo)ROZ9GwrzMePoKeQp4SeYu zlrKQpbs89FMh&O!Huxgfs2=B+tx0sO^^?*5`}KCQE=N6bzpxe;SZ z#lsO)9|+6SKZD#q-tyf=I(QeaM*;!ym>C)J>GDEsQz3d1ca846N2WB!qr-u%0lL8hX0Y=l82 z+?iAzrCc*6lAU<|YtoZDUnv)i>v%tS}!Du*Fu@AuK4NLIOrQ8IWK$m7qU3-(BT8j=@strWLh zudys7jPRDRRz_Zn zd-}Tz_Pi8A2a{pMA{JLQDoj{iLgtr*3~&?@j;%ca_ck=|3Fr zEYJ3vDv5Z|WN7DZS5xo~80;We@J^RwyO1V!?hg$WN~Hf)C3RfUu$@H9Q=Rj%utgg< zcfar+WR{;bP2dAqIh8PYH!QHh{@@sUJl%M>vNFGbSSzg#K71v3c!&K??w?c@#Z&3= zR{|h|^vQo3`{R^7l05WxeYg1brO8b>fOUkuFOMkim)4pC0@+VEJmKv${*|Q&l}vZ9 z`9dF4EF0PxOK&cbPr}t7>|MXf$VNo2xWxqLn{@@+En$jlv19z=Snz)9wI7H0rEq0N z=>6m|z~^*2_(+Jf=l>zI%0~aGuSnhRM00m?++PAvh9w7QrGAA*@fV_hR2k4~Ky~pQRXK+i92Azz z19S-jqL6(z3_0f8Klp4BS5${}cym~Tb+}k4IDiZj*mDv|YbFeL%Cf6}ReA~5*w|+T zeXe(^!8$DZO^ibF=d^5m{oTkb%O}D!wol^Ry1H$?@oBrGwvKY~cX$ZaK~8p(9Tx83 z(nkbuitzvitsfWS1BH$46Vu`|&^~`FID3`6B~F6Yp%UgjWYWGE{k8B2l>8M>4(bd& zG!Ja_M?x{*t!$jzZ6gTEXorBL_Y%9!kND4%kqv88#>L>$W#W&fT!qW+lN)?L<3O@# zi@DboPYvT~U_HAcZsl;_4=fcoyL6o;qQTV{LV8*bf?XTz5Lq;D5XK+V4y z*69qZV?vsxtZbh3Wt@A?L;e7!`{OSI8oaHBna3zS zIO*S}A_h-UMpJI7UeVK)JIOK2H$fEnL1DnND{R%@`VIPeT@nX_YDI-}m$^4@<`6AE zj`|`8PBUG1&=qigEl`<^6!CKP=|*}=hxDXoYc5cr>@ZNr(K z#AbQ4e*=j>fJ6Rypo6;fs`RNRfVPh)#EN~7YjH4YFf!&CpOzhtGvRec=EdM-a1A&m zjuOa}Z+ZYbev!vB3Z`pv>{2#Y??R{-6dKu;H8lf~=`qZB9Cm`2lv&)8U(}h7{W%e& z@Aoa-qtk1!9p}Iss<=pwh3o0;v$R1L@q^WH6omM9Je(WWDxl{a{Bm z9J-#TMVMaB^Nkc|B-e1&1x*h~SOMw@_tJdK1f(X1ggoF{&Z| zVjy2%;i+{|u62f#@7#Mwxm!!mBs^TnS-v^$G%88aC@0h&pL0O14E1vkC_$ODbU<;D zScN6tTg+4~E^Hj*AM2}hsz-6bx{>s><@r{?#8>PF!wK1pk?GWPYM~-!mN7%YTpv8C zKLOWSF}3LXBL{$(odUS~8QclVA2d;vg%66J3}tVv<)S4-9p_r1h@hLI9OE(<{X+==h2rOq{W&wQu7TDCZ!#&tv5B}uK!qiX zgaQ-b<1}}qDO6sO66FuL-O(KFP3F82C~f~tBwH@11FB9TXi0Is2-Bypp)to0m5aiX zH#hBjfMeOX*eEig)4}0-F4xZke`VBj$xN%kheq> zCS{CXqq*%9jVlpUzTLG?$w!BHaNjuIGq<7AXoC@Z^Ryv#ZU&H);}qQxr0?rbWv7iew|pcJhEBu7%6Z% z^o5ze6kGY4lhq+!0z0@MNo7sY>(@M7BJU&0=r>&t-5zGQ6Jd}r!B~gGh{h2p#v%fa zL5&y;F{XHh8L!h)Rs4DS*=Tba1>N|7GY^0vz>PQVJNn;7lz)uu3M7J94uro#As8bQ zmDEV7ObZ~lPic-tG_sL@6@KH!{(T+kCUrdJ2m&aJuLEvdU=Gt#AVr>8H5%j4JU&Mk zt2vS~)_&AfaFS883h|R6)&z>m;gm7q3k$C`w!Qivq9bmSG|tXUJVlG~fOti0=)&$? z#Q+6Ot72KvwFufDtpTq<8gO$1X7qv+Uz~!z(ILZCsL!!ezw{6xlF@BQM}O&7svxVE z=x%*bgZ&|1uY}Etke_f}IGW0RzF*$y)qke*s1ko1zx4!(ro-xkMnwGtyw8U0oa?mZ zntMU+rqCU%chv_Ul(n`t*xM={w@-Q7rq*%3GqnvTTG{& zg2*{SE8d?%F^va(r;OZ6?t^Mur^laZq>C|yV7ypmSJFEpn6YTQ7>i|v_t%*>B(;BA z>^J5;87-vn%8`-@VYYrvsw1aE%174tG)ILXnj$T7$2s)9F2;r@Nl2L^ZXdNCEZamS zqE5iE5T2PMyvaDDf2H^!LhYw>-e@iSdtCaUt`l}bp7DWd zofK(N;dj2W=7$xNTo1g`QQ3W6>9ok;BNQ-`s4QYoi^85L_zQrfS=@f$)&zmU;V2Xa z8TY^qBP`r1@Gu%-=g01W!;q5tYf+lI2@67_vb*|EHW7UfUQZ8*pK%o&Juk{5u>V0F ziuQHn!gi0&0K&#g#btUFp{TO3{J?4Px)$2tEe*BpU!}&7!oiDI1pkE!9!)L!T-or$ zJA+*aT7TXDBLz93_dHV6<`cZXFHRQbh|MSug`~$k@d!@(a^hQN%axJ)GZUy)jXXTW zS3VTKd(7QXzM_d>m->2=$Q%?SxjEE#ra5Y?*fILeJZUo>c5iK5{f;}~uN|G*3(ya> z!dyF-u-f4WX{(w2gG&1_5A0kMcF~mF?7S)w;mAWWI<$+t!Tr5$LBi1ORmhU6-;6<4 zM&})4OHP#RUP^t73f>$XQ!K8(Z9-97+MjLo)jwpY5N%7vzb5D&J)}K!?F>uj6oKMz zqJF9@>(tPp6q7!mQB9WZ2#IBJEo~B-bJ6(4cwW7_#MqAl!i`Y4j9P#z@1L_I4fsXu z8+hL55NF*4M8d& zo9wHsVp8#kQ>OxB{%rB2UY1o$wYjS5MdzwBBg@Z~G^C6urCe?>Xv4bpUoVnmgEs@7 zdIAGJ0CPOdBW z%8%w<)0fj`nw0s(ox#k;$hWq5ZEss^WLg=j>3O*JT6^BBP5kVAsnrO9f_MJDX<~Zq zqZua7fn_}w_=o0CGv@)tijv?#0TeEguW-(%%`rCLwt>wCRGn<1|0NP=| zR%h;l`bzAMtGxv_VG%ax>s_WY!Rk?0U&sr6`NM}I#`$bHWXkJbRtuK&dl$FymLYdl zX<9E`^piCjvR5X~eh9~4u!`$q?5FUG3^w4I5rz*QRQRgIkb9gV(Cwl#+h{7&H8*`o>!idSn`jXNGV_E5x(FCckPTo8-F%wXa#YJU7u%O;GTp;~?g z@HYyCT)C4Xf*r*hgec$2STVX&9jV@;zRm`SBpj0>|8Nv3Pd1ptpZnq;)tUYzuf5n# zY@YEk{?~0L6D(w{tUrl6_2`U_CM>R|I@3xTk|)4^+$@LW6I+mOHex0CM#7S5MKQ^g z-K})J)}l-X_IMihmz-g}^s8v8v$_87_;(Fwj-bY+JaDwAd~9Tf;DcVVp2Vrz2SK>b zH%9*QmNjsx_`Wj9X)H>fiJ^u0pE|>w4i_VZx9+@kbl@eR9C#f`wNTb^5;#79q3I|szbw< ziJ8v|qUv+`rwDUp@>?F_?|z%sVj!?q?4vz%IrpJka9Bx8*ZKWz6`oYOLZBhVxN-rE zNoT(&HbGD;sfpqM$oIRg=L#zb5^8+q4}%>^YOQ}|60`W5XX%&IlP)V`MbvdG__L+2 zkgOELh`-K+tZ$A`ZuB(a`x6^wcSPf4FVDW2oWWQmzGJQPAU1hRBDG*ZzapY0=W|Ot1VtR z(JL){yI~&?OP!Lyy_NrIC_?`OmebNgbA#rNXCnF7=Nf+SyW zPxwtY!IFs7oQSh9bCf%|-I|c0Fu!|RZu;Mevz;&n$PFATvu6M>U;U9cD>Huj&&V#A zX99gGE%`1lrgC+63bJ`G!*@ff)tEJX&2}(S@z6wuE?h=-G+G=b&FDQ^{NRgeH(3b< zsOK|1+`^8G3pmW0ueq^ykVic6!mOFx+qnzdsviS^M*i@~bgoEbQp)a!Jh}5S;e(k< zCo8|=THwLy0(rA0X6CEBE3y@oV0ek8yE9Sb&`V02gNirPXICY_gQ8_AyNMTbs@!~y z40uUXx#?EbdMPMlj4>q9%d@$>`}{ZeR8Dyg?$oDc>WxPyXH)$##sFvUiT?!F5bM6# znqrd-(eaxX%MupergmeO@jho2uven+;DTTp5$Q3EF-EUo&RR2yI;+5L5EJk-dy5V zwgW2`KYQjAA}Wh=El1sxT3$SrP$>(fpKD};(y8US;KF=1NUZ%;F7-`IbE+UC;<}RD z8DA!dzi(g_ZCYN`tU+Ev);k<>^Xz7 zMz(-b7}_@2rFJX6BJI%BSDnBu`U%%vfHbhhbHLX^kOvY(+=8ywre65OACAfi$xUC^ zSu66!YQtqte+jYxb!hW|RwE6uwO{W2&HhoZhZue4GDNCnblqlzKr)U#ReH})wQ^%n zk%n|66v#9XaA3vlNN_Qb6Q0EtQiUClN&R?N>(*96bb)_w8QqD_sP(0jt(<9&P45WdpQkDi71I6;qnLUZ>RyZ(PY ze|8U9t&`VqHz@&GceTO!e7RCI`%A%UMG49_W36S==rg3e1a*R1eYvJ|)e>}_?4LiB zm4{-*bgkT-b2(jFi5kBNX%XUWZ7eitL|9hI9`B!*&2XQ9-*w@FrLmC@ng{fXjoW;&+%;5@U4gS9o09?xb4#4HIVbFfufJ z@pH3qi(-MC`i5qXMNf?)4p|y#~DE}p``reF8@2t7wsSjW9lV4+{3NfpFPu#Vgd0slJK&nKUAP>k|Q< zcC%!8L3U3#sSG3xH+d&Pm%D3}RDhZ-oN5pPuP!VVp!vn}5>-8#J`KYijCMSM;j5G% zKfT56hp!{mMrMiY8P1J|MoG*v&cJ?6DNBn~^rD8~xmMdxY0i z4O_5j$G-poBECO?!4W2(3TuNnorAl8E!j_7ySg)FbH5akaG(KLF#)%i0EYpY!?wOc z#v41uuN&f8OaI~-zMeUqBU^R1`plXD8Glh0kl3142qOiPtuf&0ELL)7Z|_)vlG=&Pc4$v@K;cS|q3#(8zZe?(RDl)&zQPn`Je zkN9Tm{0Zl*2IQgp%U}hKpCv}CM|!_#rSl>xlxH5%7>a7qyj!fKQK8c;VO>f=rsu5S zs0}Vi@@En=8s7I4^!E7Fs>G)Ye8)G4Z%d{+$;BS>@77-I*XAxI!SRaMhEE7+D${V^ z1eKZ@Ff7<#{+Pd192=j7+F2_Zw!FV|(;+>{upKPA$pxN}HWYg;q@i_{X3@+Pe1?(3 zzTpb+5!jQXJ9SBKUQG&vuoLbg?seIVy*H0jj}Cwthe5Q|J5n$%OlbEyQ&~`5wof(= zN)b`F$7(EnoAyv|vrh99>*!Y*96GExT3&IgO`<3y|4ySAY zpd@+vO!(UVzb;Un>!;U=c4lpj)gs&&EdAN~0E#HnZ;UqRuUz_<{7y`M@h)-u5n=XW*eit`y!e z>izY7haLi5hGa~Yccf#rW?nKKy6p)o^WSOSHDA91!xtnH%)rgvN(%5Rh=z6>bEZ&a zbJsgdD(LsocqA&lThp+XKf?yH8;d(rxEo&f(&RD6u=oS*bMy@$ZA35ufD`aMX?pU%5%%4qT*O7NHanXW1KL(9yGRxs_HHg??72_#-%vZ<(2{LW4iZzV4-9U z`wKcZ&}C&J1D2*xE&6RcU@Sb?1YMs>?)-;IAnqjF4`R2uHCGSz0YVazsM5Fd*O~)* z^X5AdeWVXEXdsKVq)BE_rDVYosJS%R1kM8>>X=AbXd@el;QGOQOA213oQDN8NpOJW zHj(B%K@DJj&HFFxB5OKrZUjD+FX*_nKD{#_i&2ratDUKoGQaiZ=-|;3u`n((l)dCv zV|*>7f>J^;E|bQ1ldomGoQ-G=d-O_K2w%J-KT#|94`31`;2b0-n9-1ht!(4h{UjT z3+Ocyi57h8Ch_#-qpl?p8b+&rH>{Tys9|G67Z!5JB}+6M_Uyy_XNY{eV&mC*nYYUE zwRZ0oXFbL0l4Gv7vjb%pD*p1=#s2t%PFJeRAfH1LnX$ZKzI*&q6;giZ2!r}yBS1L8 z3r@}ASaej5VjKYmk^|j=QmA2DjJ^>U^c00xTriko?r-TEr;rtl;d~K46Q3mJiKBzx z>ORgkY6d^TB4oPggb~K0ng|CazT$r8oB*HW zVV{LxasDqe^v|0eNeZ;%=v;(1 zB(s@p1)p%4kURQM&5HH}w@CrI|GZpz3VG#8S#R3F^aOM3G3fP83HcN|-ktnr*&*!# zwXVgnqqf^~W5|4D{#dE+N|MHQ7tGA~zn^k3B6mr4C#{Shbu79qer*0PV`-_f;#R?@ z46R=eaHWR60pyR|*+<`^7KN?bGPVFSYR8^85;~%sHUvx=zrIrou{v?81-D6rWRO%L zV(P+AyC4shXI|!fL|#Jk097tJELDDT1%CR4n@~NE8Vx= zLl;ss)+K~>yKLjrBLI!I{?%S+A^K?3tPGc1D3;+8pb?M*b|kqtwm)CBDx?b(!}B>x ziJ~C!WmwQF7|PtT_tzU30vhL|(o2aPTpw|FrH5bU9(f;xIa)TjM5beyE+2ZW{a5l& zDw(y<1(P&y*cr{RhGkc>?oOY5jf@$I1cr)?h=41% z7rh8RPe(tG{sxZx+Y+uE|C)JjbT=(Y=_NpVW?bZV=fD&A7_B$mmOcJ&*VLLYGlr#FTB&oX?=HdWH(M+F_`(?EE3yv1GUX>B_HWW9RevAGxY6d+_Wx;QxI)B_)Ddr- zfo7#`mdlyWW+Z8CZ(osryisSyk>@gHrtmfS3>VaHta{||{D$)Dkw+UgtvI0yT zIi0~wjEaP4%2VP|asi1Z-rho#I$+&WZWg11&p{QNB9J2#uJcGb?}e8SBWt6NR#t?Qcu}7DCaN`m~ZZN>ULa`WEw-m|>tz2}+jJ$|?=U#W2f{$b$0((m{>^|PF}vdJ%;a%4~a zob~??X>Ehk^#Hk4nE{7TS0>EXy50ulwcz-vdqnT)2t9Rs%xSGh73_oZ(a-0S&xrSE zM;&~%;%PxEHK^=F`1z^EtdQ~}6F#KCU#rd(pCO)S?Z>}P+s$XomUtowdp(k?)z}Bi zPeJc@JoId~y6t4_trn8YTr^5a3+G1oR2Ix+|7Q33__%5m6igk@V}VRrBTD88`=ghn z>d%r#B|Fk2>@%^d#1lcU+dTuCJ`Ev_Y3ncV$>3~gl#=l}_TThlbx$`6Mt{_29OOKa zrYdEuXl|5TFEUV#GK@D}^ieJIG8{BE1iBL^4#JeR!&dC9swmC1PKT#C#q<&r*Rr!D zaAn_IwES+qum2)@gBq`6;z*N2!lH0{yOw)W{H@*Re9ou8<;U?KG6Hnn5E<(a7Vg9z z^w`&9L&rKK2^O*I+%-&yX(x%lvnSI}9M$Y)H0?R0A|Ts-8lvyuy*;jzh}JnC?CN&> z*<f#cILr1eI&2g}a4$`H^0*5Oaeejs&r_FTEvP z%68X}bnJ6DNl#f^Cgf~`o8R&RbO zy?k3)nw0Bks_&t73DZsmG;a|rh7x2y?&a$nZRM)6tVP^2x-5@e=x2Z0(bOi#81v<$ z6XwPA;huVVFhR%e1u-o;S(%<$H#izNo%raeqsiXBic8nE8e?>VHrMKoOgb^fK54~I9~&J^FL;qjI3X;uFR)(q`afP}Mz||IX2Ud= zC{%~vD1X)Ve8|Q|pi(N@;`_ePCC!Vpy4|wl6`jjkNOGm_48Ev)LjcZBk!6 zQD-hoZCt4){+53og_bP*{2KGOKXXPXk%}iYo3MLMr1)?PPEs0vdB^&nu$Vx0#BM+K z-LxL@sOiKv9e#NVb5o;!$89P2VX@y86kO(|uKOI!Pa?{U6 z6i09=F7X((TqeddZmywTH`sr=-*NlOH#M)Dx4s`;_tRi5=9%!C0ju_f#Gk(;D}FPx zXc3~{&W696y?2Mx(hPYG>^i7GDSbf#J#X-__L5TD7|ZPAMD3+IK3(CglD_P!eRGnB z~E~w z(MXIAyWp>YirCF{@dJ0htSG7{t}MJ^%y>i8NU5(I^q{mZoSF&U4JA}dmpSXf=hyZ& z+Akk-79P8_LlwG5zgh&QRmw6ydygf@(-r8obEoShj<&orGrhjnS8BKJK$TOO%rV~c z)H4^>2!lomAx-yFN5-e%6#plye{SnCT%Em(vk}cFy!63vr|KEIfrNi0>W5-#hKEn@ ztCw6UR8GCHr>1s=()#J!%N-@Yfq2G~9pb1d(Q`-_E#Y)%|N z=o7yVdD_hwMu$P;iBu&^X&eca9anfMv4c(t%}Rv3;Q6zo+C-OiBjXRGwd+?ze`vg- z(T1JIS?(mp>%xY29!)facx|$41$q0kuUtL^C5y)6oELu6 zq<<LWj&ucD0>2rl{ zY|{X9tpr!>`8Ou9Jp4O^pF6W9X`@t36ys1EXM6Glk)>)XvpJG4TxtOds`cx~OrK=> zw8ZQtKrwrd^&K^Jxt81@BXTYzs-b04k}eKNi=5p74mu3#+Zeo2tpC6)IUW)#GCFYx z5VjmP{Zg~HL@zk{{3_Jaxgj?C*y;wo-mG-umIm}Y?RvKG^h2_2#Il;q4@GZo(h=ub z;`f9msFt6SX1Gb%I;X+;xhd{azOEO)P?oOp0SJVslZ|H%{73Ga4*xdrvx)t0-HF{d zu4|fRJxUvSH#O{f>eZ0T(?JIVcd6i2DQ`i_0|UK1MH6uJ6X7_|wRLUFz&x*GB$OXI z3hb{%LhWOUsGx`1OK^S{f6=_;R&&KFzOrfB5dcezs~g| zO={4SR@_0OHQC2h_(d{dasA;qHyEjG9PJVa}r zmld4;zD9a>SNi)TilXVRy1ZRKct2zgL_)u&VNcCg5ombQtIda4WcIn%4tLz>as!~J z+SN9y>y`=+CIze_5fAzK;X}4CGfyXLUPv!0j$6F)+>_0l__XGcx{p&_$6)p{`zL&f zxOdNR=zY~_s{=_sYS(|5&S!qsoK6aoNWNEpZLN>RZ~7Ei*eieGaz}8Zk&Wf6(f;e; zL{h|Qw&&IAdvEkiXq7Dvz}DmL^Y$^@68GOf9bazg`~cUHwQ(JyI1%r^!qduw^K|IR z#+3B`eB@kAq8w`?iq!q)i28Ax?>9Djyit~ME~0-q#Jd=S=oq#fG~D-~a5LU|EA}?p zF}5J#0a3!;X5;t)26mMly^YN?J+i2tc_ZqPhVE%XX%$70zNytgUcu9Khv1}!>~7VQ zjZrM5mnhX3#-Ve)nqIL+O9i+DuAoCTk4Wn5iFkA>`GHT8Wn(PxRtI)~5_@Ib;lYa6G3qa=|1k-O=+cWgtA)A0S=$H!zSSz7 zn;!WL7$PS&lO$!Nx2c!}l&@`D<} z7vzXcb%UzY^pz|UBmE4@-sk_CGoeb*tE}zbEc+Zgvveb}LslKpj{K-pRy?!mKg0Di zNj1e(8j*Y6XfY|WYlT8@PQq91uFrl_Hq5JPEwd8yw(Uks9bH;CJE3CrRc+g|&uSTy zf{(7gXsVrlx$u{h;NV#**U8yW{TsgB<)Kxi5>n!I8c=sXYtwH3)lGnMowTPsD%S$G z)1qy^*HY@N`=IzO@uH#hQ>rlMxPNhC;97Y0tNReCg%&>^<8d*pK&5t{BGzlPzh~*S zh|q2t?>bA_q%|t*p_lfrs?lq9r?4&Fu`QmH5ps9_gNnXw8>f@wz=?LAN(W}wP4Vsf ztY*^f?J;slSr&hk@2QNuv(!7xC0Tf@ZWQrmW3qoAQu#V})~@rhyRM2`uKK87bNYVo zjzV4}JHtApHU`+&P9N`3ZW8|hY-*UJ+S4_kogDAl=4cVQZ{;I%B%~2Kf|Ij-bO{N+ zmLo{KY`)>ZjYNwtxA#7~6{|z69XOxRdlhaHf@Y0Js>(?e*1ndNRL4+ADbmaERedpO?xlTblXO3HQS^U zz~4L9*;u60WU=LVvfMX+FFbCz=@=|;kZfo$GHDL6pW>5bjbY-LA~juza>h;RCT;AL zXZ|amw{iT^kFs9eT=kl+Ej!W1PNqf@hh~-3QmBz+)dN0K^*$?Cwwh}ICwX`HDF>fy zz=_=lPtF|c+ayeAa9SGJC`@W#9NSsqjD{@lZf(i<$MV>Ym^7SA`sEX;TAtQc?B|?= zw|&%TT@GE+Y5IM6zuk>iC4YFJ`N~V>*o|u{|NUaj`gM2&KWN@2aN1&aSpQ#opS4-F z>taG2RraoH%q7BywHgKV3E9Te?+&l(OQX3h8WuCP*!`dP5l5PSM}}@VYyj*T_{n^s zT7So^14!6Mvv_daVa1i)Z`K*k1?~lhzmM(8Q>#xsENG|UW1^R|*Gpy}o8{|UK2a_m zexlG|dBw9>yIzshyj9CVZy7h(+=bfeTlAdmHevaWM6=$?deqKyRi5?g?`VMMuKu?# zA+63*Ne(%@=o~JRXwPvruckfqb!4R*25nn+e-xgZxBu=#0b=AmzYUd3_TiWApS90m zBV~n|Kf3KRPy7QWHQ|dyrgmzp$BZG+G3V02{N9rj{_9V%b97HQ;Fo0lqSmwePn$bH zG8W}0yPf|bOtNG<((JbbZqo6CtjITq*M|i@z>8$gvJDeP(@pIIFYFe*cX=hZffIP( zeX-O{{cKUhTjQwSnA68o6j?R5!=q?IgOoQhTZ=y2*|`pgG0ck51x5Wo3S; zyvVfIuU?VlOK&H}C3EV}2-%Q*@JwMTLJ*N%VYa{fbNKR>CsF!4h|(7(2~>sZHb zGt4R@R-#|Zh6-$@8&v?S8o%}?mHv9&GJ!V%*uA*|jE47X{h{e~wiUo;C9gVgv`dbk zc)zNGO+0!@+dL763&gkmV*hOIyofYAK>^ObYE?Vk78~oM7S=c>3T%xyvEV)YDm$#3 z%U~%I@gugnMCGMk`GgcN{6)E~Sb7~>G|f=!mFU}wJTY~`EIm}%L(**-TLKxA3H}?j zcdon@c>Yl>vMN=<4_=frd$6NJ-qSA9*`RScSl;+&G5*YP+CP@pwwaetS2^&Bju!U+ z;U=2TuQZcP_>r&p^6*v9jpf4IqVStfcNQyt#oJbGQvEsV7c!z^V06`F!5;l4a^14o zXU3)E6vU=WP=S?l~LNuF$dfltxbS1x?m(t=V|o z1!%%UB+>g|=RKwjMF=^(oWd-_Vtqq`{n)$w!=eYVa2562l6Dac3u|`l|0DFWkH`Bh zaD%n*vJsKt0l^`$W8queQ8On^6PwF{Ar}78YoiN)1djO!Zj7bkTF|yuU3VvZiM`Le4|D7>`-r+IP5qPOTu^+wHEG_v)2k$%*9KBC9 zGCV3e%s)z^{Y6w1!_3{;O5~`Yy|i+yJ}%ljAcPe}Qw+hGNbj#P3>k@H7x=TKLiVu_ zoLU^(M&I%+AUtxl)YGx0bz0ggVXMX8PL*_9N`6*u&fUAgPxA^pILb;A*2eL}NAOK_ zh5vS|@A1}UPqRbA1LV^dDvtWcNA>UY5AuyQk3?3FcN?1fE+|KbbE5qjEm>p!Y^|u> zQAZi)aZ&%M*m<@+FHUUK#Musw8<&JLSHxhe`A-SWre=37BEspKn<~E!_)dH81omv( zpHp`d=NuHlGk0VEWUL%lT#@>3Sa`H+N^nJT-)hFcmoSCEnEONM%~0SpLh)QuE-RPx zWh5xr1?r`5oBSunouTP=KT*Bx?$&qrKA7l+98r156aF(9wkMDn9UZC}^>SD6{C&L! z<}UW1Xq3Bl#Sru2ef<^+dF!p1zeX^5M@Kuy4Qv-%D_c7^TQ&@$;8Fy?C8=1Y&dz?i*O*0Wp~89=G@V|05)~EZ*bN^_R9Cx+#O0i9R=;9V+BHU;K zYmi&I{GPN$Z*QYt_+EBUm%vXh9w!y$CFK~XnpO~8?H}%{8e$g{60&yJ?mdd<))dmB z9&}vGy6!sC`!;r&8`TDG&sj8%w{)_%7J3-ic-wc^^L}CnP3)|lsHSGtX3l2bZkD#D zZ*kr>ljSktt1ZpFnIgI~+uhRlx+Znj?BXkHUg*9n)BDZ%0!VwE&iYdEiLRc$fq;Ky zdQZJTV7k%N)Kpxy&-9N>i;u*aiHW%2ipgL3CO#Br#=6FTDvkdLvwq)|Ke?5YHExZC zbIu0}MZ#9r+sQ?|O<}I}#UgAiSSBJf6Dt;a&xXEZ2?7qy8*$9UWU!`9jj% zQMcXCdHdn@m4pZf7m3xO6$4r}#vboK+1boE4IgxyII}Yk=i+Rx-|o0)?CAY_GA_F$ zp4%NUczU>8XsKo(?d>`Cv^%u~XJdfa(*)M%H64OrDPL34{dcirABBAuX6u}UCC-dJ zJ6~O=9Q5lK;`~XkrnTt>nk4)1G`0KBcSWD19$XoFY_Ds-`Y!1M5&Ka6Z|)mSBS|LK z{^I49dSNT8>ZY_o88yw|kpRc|*RlE8&bK!ELezA13U6s99WeLy(+5kPK)-!f^4910 z^jw!YSo{f4acI~N%gEqzYB@p<1*5LG;kUP%$Fd#wJC2qH05U4fZG~SPLG_54n23l= zK2|VcTZ7s)+=Q0~XtF_yQRmu>o^umk@$wCa|! z(Zc*DMh06y3CyDG7p5OZu9yU8<;{Y}68A@khP7qo1$es`IY{vP$Ryt9iye-svB5J%a9o3-AhaqiDkyA6}paZy_aPy`MMm zcsLsQtNp)6@dG)IzDpvO;*ThKw>?i~Lqo4+`2JGzbrmHU7(Zl}w_5Dc`G>nR*>@<% ztnJZ8>AVJV!DNJNbkM2vnDBc{Wo64;!8(P6CaGfqF89CYe^@`+US>H`PRr5ol{90B zbWRgFvN$9EAey7DO6h+FC&u603lIC0x+8+EG`pu!5Ov!w+~bT)(f*Cl$L7YzE2!PoP4+3h)K0|6b<} z)ByfKE$}Z;2Ry^q1BZYH-~iAFI0MZ99(eSZF#uWs7-$2iKnK7Ax`5R{4}b!_03GN9 zOn`m>3y=R{0XzZhj{)w$C=likL;%l$oxlX(^7r~z00>M08-Ob@l_F^8VH(!*1!O$i-D0q*o%&-XJ8Px`#UfV^4^2Y zu=uL&p!ytW4%z^pK)q=&s=~W+{5Tj4?g@o9vvZ({YY>Jpe^dF_AsRn-h<^k6!Ow#} z@^3;p{Cwy-{}v_|=m!5bbd!Gvy2USmUhxZ|DgIq(nqLGp^NXRM{Cj_yYy1)@mtP9y z@ynom{(a~+zZ|;5uYd~ql~57C3TqKF_xRONDZd7~&##3l_;pZKB2>d~fa>^vG&DjD z{3fW0{{VW(Z-!d<51}^xBdCMl0(J6Rp{~D78`RBjhaU4gpdNlF)X(pNp76V&5&mOn zjNb!2|I743FZg}X1iv4e{1?zAG4Y~@K!s}vGH?Sy9&RFV;0FW+xS5~`KO`u@k1(+ixNr+W z8Ez%0z-~sTmI1EQjJ~-lh3wAjIyB5N3-dbLj$6)t+u*V5_8|(=cOk%vBuorX}-f;@{hKk{p zoO>`HDuKZZ72eB^JD@Tcx(pMba+p{FV-LKS5vUR--hfF^HB7z*Q=nRy3N2IOC+lIf z5SD-%VcUB!A9?@_;wO3I0Bl|b>p+j-^>wf*)C#Yw@>-@e!Ddi9tp5l$fHz>BQ<3mS zcoS?0Z-#o|El{8MCfE_XZrBJKfQ8T?Z2SZ^fremRs9`aFav0Wr23td;uod(a)|-Mw z&^R0^)@cJhgDn?e3wSH6_0MF>6ikO1Ct&3d@Cs&_SLG>K`U|Xd4wm``bH%h4%z{_J zt6>=#@tS~Tp?9#{FL({K2+KnsVBRE*iG>Y)f;rGy1^=>6Rbvx z!5ZW-*o6F1aSD8ZoCcebGvGtyEcggH2eu&R!B!*#Y(p}^_P@*pumi~gJCTcE7jg;g zMlOSokt<*iauw`FvcW#2v#{Yy4%m-)3?jMU6J+}kGW_DZxOF$d5hNcRMQ(wU$UX2S zQUbn0{9hww;4f@yXbK5_gG?h8;0#g;&LUOdTO_{9D=cOXnMW3ocSt?>UVQTm&wY!? z2c!x7h%|$rkO$yr#?D zjGsas(MO@4=&#T~R99#u>L@f3;vnZSv7ggJ^i>!l zu2y(TOjH;nCMk>)@h^U3YZaam)fAo+RTN$jRTbXvS6CvR9>%Z=jF`^(Nj$~*MdAD= zp5aW8TDX&>)7+ONlG3Z+*b+vfD!nGrl%_~zr5RLdmPAo{OJZ^7NObPJSnCTJ?gD8A z_Z?{y_dTgvagkJ`_<=O`kyNW#Ay)l~RHyivB+LD>j=MzC;eI7)aF6NZk+pMcTPFkgM(r5KPt%EIF%-x8nPt2mb_N6o~)+WO!nkFBztilVd9>U zw{u#^J2>rR1x|;Zau>OY+fAA9R=Mk~X=RPBsbDxuYxG%_exfA3!O!6HvR{j|)T{*d|aZ|2i@vOfD4c{TgD zg*s&uE|0Pqmrpr@QJAH-C^e0gBHRPY4TWY(PlH&?Lkd~3h2o9tps*CWDCZO(Q*sr0 zD6a-6M8%=s{*y;2b0d@_j51ESrtp(;y~=BX`IX9KPf$-yQeRRt#NO-`^)+>h`i449 zjTgI`8R{(cEp?7MMwq8g;W2ESut0rAct?Fscu##nSft+gK=snf==@0iMEy+tLS3TH zOhs0Gr7lyyQNL5)V&Q$aap~7+c=S3AM007-FO2XTy+I?Oc{GiiGz8735z$*T5_+44 zg<}oNcW4x}fJQ|NX*BdM9xb9tpv5#gdXI*Qg@KmPB+*iu6k0}Gf!?P{qvf=fXa$Xl zR?=8#)n8^6T1{Jx*3e|oTAD0cN0USAX=~61nmpP_W1~$pEdCY7K_Ac*&}Nz<`mhCk zMB}0@G-b4vrh>Np(Lhs0+i7dj4w@R;NmEC=Xc}lYO%r`g(?WY_JhWGgNPa~7XacmK zri~8J)}ezm9rOupJv#Ka1-j@kO%ENR>7%1G1N13v1Nwrt1)ZQ7p_4Qr`tmPhjJ~3o zps#7B=oHNieM2)xr)d`G3{8a2(k#)pf0?c59L)-yr&*&5G#hl0W{-ZLIiMeDj_4&IrMSm1rL$w9h(RG4cR7Y?FT`$N(bpJAwr$(Vo^KzvcB}sT@H<^I)7{fEFQ-nO z>F$rRjC?$HOx1jhU};sdG7;vwDp}Z3^ zlVIjC&oR$p@*3kVb6MtXrV{J`xW#DZ3D1dilgVg5KT4CJySO_7cyzkLA1Hx*r{H+Y zu3eQQlHUd4;CXYVeVC7BhS2=PuTi;Zba}?UmHvfCZZUWK=|N_?$uv}-F}MwSo8rySAK3BFIF#nk+C(ai&0pf^xk1e8R2p*hp~KM;@klO+eP zR`HLnRcFf066=+Iso5WyYb#XTzBzsGEq|HNh}&~MIs zuPb-GZT@K(A}7@zJ{C%9*e41geKAq)B`*%i?kmMu)>hinK6V?X1>tC~-pNDdIIXd< z-$X&eqY{ptQb!}uGP;~mWjZ~E|pTlv_82hR^Z3mR1_zS%p5fy3S5|EBQ0 zszq~H9HZ^$q9795DEO;ZNnzb8Ra*VCT|HEg#3}3} zaE@5o0r$L~RKQF;XJT$q;p=N44et0tc*DL`PVC&oG;O0Z#5;)sv^{1E9~XAUbrLxV zs(QG0}T>Y0%UOGMJ4RIjuKZVWxW zS0%SYYn;Y$#to+?e7%e4C&W_juud~FZq8aROfH#Qi_VC&rxMok0h&Go2e}Z$W*M*w zLEIlEI!C(0LMnNj$Wc>i(;jL>27@p<^?79UWo!JbAWN~sBpi)$x48hK94eIz>IsT- z_hYZg2$nTFk}y~kFM^2_xopoU9P)do>bjqU1IshOEH3wg((p#>cz51w`q zrKyM6W-Sh5>dq654cV1a>8d4BmNPWyW+VJ8D|E6YKeAwKrYhEwIMY`83NWh(naXlm zIj=!}_QuD+DWT_I#a-$*M-_58+N|1KIQ>iT9k z_c2}nL_9u(&}rS;@kSBtxx;s;2pUpE`2VPTQLN{+web zW-*v9T^Oon$WdBqeT@PGk2zm$(~xM3zUPDsH#ci3*VZ?KBYmGUDkjd*QBkYbL7vO2 znE~4)b0DG10>;ns_0O^#F6|r?<(#IlUlZVWS-!AIIbd2K7o`Db9*^!w=RBBpo)X%u zmAx91o|;MZsCIq$(dN$~iJu%LZ9~_J7j2Z#6njAvBg@2}ft7!9+QEo7)&ELJN!wW- zefVUYadH35872`)PL*5*<=G+^WAsb#tJn(s?eV8equ=@1{cbRfXkT7U9!|H7x1^WU z{Q_(m<2U8dSlukw{m-J{86ARxDG*_PE!M{vV_H9nDjjolqapyTU9G!&bdS7 zrmrsJ(9xWxjuyeV)TT_|8?sDK3>@9MK3r8~^X_F)sPV7z<%|i=I;(ZyU zC~ll<_Ytw4w`=83{J;EpoZLr*)IbsE989DqBkWG}NaIO&CPl@{jSrTjrp)=PTpjQc zS5YzIR4gSg4|p7+EW8MmuC(1PsK*s9qOMjf$s$(m!b2k!XlStLse{Q6 z?xj9{yMY~?#yb)2PN{~To_Q7dk3>^+>O8zQqwW zQEo-0c502q%%SCsT7&M)RfEZm0!$yoe5{qhB9~oB+N$wIh;1R>&Sh!1M$2LaS3$5) zafs1Qg@}bHs+StgJeh$m`{2Olrmhw@nrf1KnzbuBw4Hg#JC+nH3p+Mb@#o`hi1}sp zUj%v!sia<-rCrm0HCi|*mj2N~$@=xA@olg8@bEb#-$qO=8IYke^^ikyhn}>i`^0Kn zu+F7JOIkB!pcSjFiIsigU*v%DtCW!xQrSq5y*V=dW+|DkLZlr9f12bYx3?Iz&qL z&}YhXJWYA~k&F#fF&9xo+JnA-EFH1|O;VAjm`P?-5OVss_^)TJ%*5*?0pNpl)6H+* zs-ULo1Osv!OQW&`kM0!d%3}LzVK+jP@Dn=Ie2l4p2Xy9Rqk69h58l0*b9%#XXfD1; z=p}!6rfPfSsd?pTq4#Rna9z2ID=zNeEjI?bRvyW@IQt9|C}C)N!;w9^jbt+xR1$E& z)Vyd_j(=OUdkJJ2$Yw#HeZ=$D+v$2PC;JI&%^`~$;NMg=Vh{WHnawe-&fwR0)0p&T zil$uzcrclTZ4Hog!nS37P=puB|2@MG`&N=8VJp>8@Tz!+`Ff5!%uHsLoiz3dmg%BU z|L1P|%#QvAD=GYFq$AvP9rD;8 zb-#ZBDa_);HOd(|Oq=3PtNylYpGPBRY{e_JXM4yv7rQi#|Ans)$0uWoj%$<3h|LbM zBWR0|WRu+@Jf~T0T|g`Cb7gaKJ~4<9`I3+LQJ{^xj7KisjeY(| z6y~T7Tmh|U71OVz%`$tXg*=+`e1wlG;Ne7;O@q}_wwAew^(cZtAw0R}TqHUxNsR^E zPsj&VZR#-v8hZ=@k^)wtiFbjg*a#J@h7pgK0w}`xuD)F_N2Qw(qXDm3 z;Bn@W@xHrM&sAGumW}lIbH;To4CWZcxOd2OokX=yr;_Osh?yj`rKor*j(vgVH-p zIc>5~dP<)qz(MM1&ZO3MhEmT|q*G@1X51WZ-Z#45_j7IhDXJ|>+Gscf*~X$9v%5@C@}qSGXgBr|}`>`Q2r5=_sI9 zsBU(*c+;)=SYj)wh}AfSccEA{S@Zd*6tS^GXmf?RS|1{VB~w~VzvFoM^{L9*K70b z3FV+#TtIS0z25H&+kEODaHdOqhOddh4ZExs)#)>0KbhWX&V)@avu2@wvqLG_e9;0# zC+NDVwT#&DMw+sDkHHdQeW}^Ok({sC`YB~`;?Rs=U#bC#q`BhV7Ek=Gbp>f-5-*SQ zz@{0e;L;K0?}_QNRcW#bRl(NH%Vga@oy}2KI9-gF)wisd1pGQEE7%cvktkAR1uMUe zA>0r8*sLd;M=PCd&TY$2sP0%Gu|4`O&4baK{VGazWF4`5(tW~Do2WPUX3VFOx?>F0 z@AbR(J8n?~8(Y}9GIzs0YS+r3ZBqc%$p2OQFQ~X4jn?(%cT!we6`PGq4TydqSrI&; zZE1X(X{=FzuNYSBGYkUi{;C%rk?YWD6VTm3Da z+#J~2oc9|nZt=Twx#wy=H<5JM!cQi)5ja}*a40ikc?1B5kG2MqlbaCG&S!OMk6lvPFDUjNED1aU8sH>8?QaJ+txz zV~TI6f7sY>Q_VMvlmdD-8>0+!utK*`Ri;q8-_l+@u4ceHWTr1emm`(X-786M685b< z^0F)cx`C)ftaOSS>2*tL+;vU90yDF8KW#*1+bHUZ9?@Tw`MF&iwx?M8=}138Np-xt zPi3`YOKxMJqm7cqQXwT}GT!@ZieIH#=Zm9>&X*cd?N@Gv&y&gEu*xAG_d|2la#Lo@ z+jHMHElS~w|8BAL%_v*T_Tj%djh3*pzcIbfw>b=jE}6>vAhwEXTHJ z56WqzN=~V=BbetyzhXok{n9+-jF>9H}jj`Z8% z&CIanicJ0tm|y5|v~W-P|iW_K_vUr_4wOT}c{Aylxk z?Uja%-=+U$8eK{lELU&>bsl%yYe0ZP%b2!m#nCYtY z{I~tN_m>O3B}%Gjvl~;jzD<%c=6Tim(bD_AFeA>v1GBrB_ZL0l^oBPJBjBG-!M)9- z_XtbXUo*0ThWjdTuwo}BnKyd-oxMI?&+Z34ZJO>k3_5W=wf;`Ebh_ltmh{vg>Mh4q z7(It&GNS_28U5?-J$&Ab773XTq!)*mi=&15H!9q~ofF*mb{XVr)t|f7w$My&?8*j{-UyTqU0^ca z^y`9RBPfS-*`1avPmcNWzI*n)tOqsL5-SL#jT-{w&f$@Ns!Bfx@*iZcbsE&Do_ro5 z^`{Ec+&tR!_ju(Cd^5G^XjPT`sXy-J1~%poZMI?WNMb9o1U(Xc_TK?bOLGYK6I6KG zXOd+&-_dG_E&U0HrnqdI0hVpcnFTzbiv#pjXQ8H|xqRvg_=x8o#r{ z2-#gsqHs4rQoT#<7xTDMgF#Qb!x zyKlz<3%Kt$^0(xvc9Q{e!nc1=?bcHipa?XafAgPE2lp2DxtO$t;>SYHgQJ_TY^8-C zHh>N-b^@;qZswL1EWpHTMBFPbCoJ;AG$Yw`oOQ*M#y4*{7%<1LAO28J6-RD(lcx1P`2qpsCSNl!0%!o8dB8&uS zD(jUflFO9W#qae4k0yNFipL!oJ2U?^U76hGm!ovcBWB?kdcj5l!(xXnWjb|K_z99 z(@td&oobZ4M7B4Lqu@l7Su{T~!EvUR$V^M^VH1r-H^Xj97tRWrN~5ih*^hYy5nV@d zj-FfcmbGCz47+~=_ZsT*g=eH32y~L{PzjH-7tF$BfkizzPC0F4X@;0o-~-JwEzXje#SW%9rcqrlMdYA*0=pDTHweuT;vlb+Gd0X!}WN;+WA%?T>uoUzt8M7oirCxVv~wn#%PM%b^lwBFB(O%8 zVJ7P@FiOa=)n~3bHOIZwDJaSHjH!B#yTecd#t~Y7MY78Z%MJ2)4KlL56lC7=dq^^V zvM+AaD_gN-S%KuWpu%KZYZ!g4*?5hbu1q`0ToM8nTcI2E;p`FGxFp)DVUtA@PU;ZC z5(arG4C)jyg>z%)PY6QEbdK_9-hQhq(v#f8szD8p*v(-ql|gkTLcI1AEp6Eg#`DzE~{o5U@xv3coxsL^|kkqJimIE|Q@ zqzR9GbXpJfVF9cvN>M~??_m+~aAVgJ`~qUAx7SJ|I54LIB0$;oPg$f;SvrKFPt*Db z3q#>9?k*v)y(60kcyH&3HN9TPBVN1Akk}3~%3f3Kf&AVBULn(CwAo*sTQ5#u3yl zH|HR{Rt(w3I;}xAmDmxyPuQ&LvetNNY%KU9wQ=p@Pg@qo48Rgj|zTaZe!V;jYTzptxAh)?%2U#71 zi*02}v0UJ546Fw<*En&bzXu}`lgPpsi4oFA>`I`#lJc3)Z32i72YMla687u6BPz`q zU%s2%KROhLjFg8Cl!rE`_YSBm{oZ_y6sEQ!x!lCCM7*NvBJN_th>mcS5l6WuKHfqt zat{PcE_@*>!|{;IK6080xV4aw#-Qz3xJd%JMvy+E@Q%I+f3}2aJO%u!2*4Y_a~U;8 z;r{UZ`_BmWWTQX6I%x}cLO&58i(Q{S@cPi9ntp$HX%Z`Bv8Uerw159t-B$(txT-*0 z)Hele*C?!(tTu#pcugvbvU*_S$zm~n3tg8Lf&YLkbc6zRNzTM@S0GW|E%kPZ6SF7>DM9R zft)?$|Nn!D|NWlNNCi}Eh*Aa&D~zfM&k;Cf2)PRT93)D_4{;vSlMj>}ER_#I2PS98 zz6!(i9t@%gLlJ~ihr|}NWC*$n=N{N?NXP{?fAD`cg!r?vawo`| zvq|;5L%7rFk21pU`}u=1&W9HHm*}OJqP8{wNKpn93=Ief2nr}YmQ8XNgwFje0|;ob z9|#B=Fl%q*YUOUmXlrHf<_Y*Twy-t#U}a%&aah#x;#<_g{echZGh`|tAvKvuR!Sld zjZlh8JXMBcO|?n4!jM73*~2&)M2W=0;0%pUXP@ z+t@1&V|i8r`w=c5A`rF&x>3o;&`&~2 znUgWhdw=`Bv{VTOF^t0}lsgx7h=;nh@Yiacls8nRMU+AplTkGWj-Q5PHzZj(~eZ zSr8ROpc{M?5HEe96s+tB}MTlpBR06MFt|Yr*n70`jFj7Y+v6qISZ-*19l!OmX zUl`w7IU-5LwpX2<`J>Q+oxa@{w~04@ zD~ugz9ds1iGG5pRWSw<3H{{uQ{xc`P8{~@Jax4ES@L9tC0`v;sG9ctxxCR%<4yma& zA3lf!Zk==Xo)(M}W8Ssps@wUY5xn6h+i5h#PO_P=hmYH1>+W#jWINJUcjd-F576Im zZlMg%dioXPsw2S4c1)I)aG20N{6IY|qwL4u`Yb_|fJ0Cm?S(Qt576^hrj-w=wdpSD z$e_1(sS4p@>L-T3|4Lv_d2g0W)J#(BDm)tehAphzy%h{6yf=IEhWhhiT~r;sMr}U8 z)6oRb=K_Bu{x60^Ugw9gLHolVr5PW5cOf?+iwwhd+j7NP=iXx_&)(Fru8*$82>ZM} zgGYe#C$dL3RCf_UsthK9AE1duh2fH3_XI2c;VBXLh`-0HM$b{B%+!bgBAflcPeZoG+LSOCVx4Qw2~BjU@Z)0vsk zdMoR7yFu8soeyg>NrwyupYPqq^8g`Ry#34kM6_m}JL`C`mz)|s8>PYQt<8&UgUQ4Z zXBz1M0sM{a29~WSQMM~oH)rB4>IzT(P^DZ-Yo$U;BYWw1&8X@doV2<`EFl)5U`KeO z(gU(;>G)BZXDE%z5o*~hp`~mJbJ<(C(t$z#rGw&k{9eg;rQ$M;8Ltet3)ToHX(&9!2-Xk(ob?$zOQVJwf9J0yF`-i{2bXhc0EG zhlQU9ip~r)gp5iTjY2~!k>lYE;{nLlU9dC+KFNN_*4-A0&njh}4*8$l`L85}pV)=N zX+@W8kMBxfX@&bvqHAAuDmT3+igy`BrZ$V;@S;;cz(brEQ-U-Ga;L9KAK_7e@@s0f zdu5+)`S0QR%e<&ZH&_)v;>kaRig(*WOAGX1%|A*Hltu3WB2zCgDLdM#0yc_wABD@E zxahgAxnzs4I%RhkicfCfX7S=ju%%3DaiXyXKvzM#4(u6KaY(}cQi;%On4|1T zraD9}lw<1B`4G*QNppPx@Lcf@>6sUl9CJygu#T6fLDUV@6@9?tG zW-OQjCR(7mMUiz~kjOl3{bzdvlz+%Ii5~i1Feb}%;pWMZz=>)zP7VIT9ezf7 zGW7GGA}mv-IRt*stjIs^ATojaMhY9pr_V@TkO#f#{QHDFy0<5Hq|Z%OV9Eswj!l^et+xPX(L#TV z3KQovY6WLWLtG|GQwV;0A42`^X5VXPM!tbb5BPWE;k>2~KM47)`SfREdG@Cx^0U|^ z@G_d{`B` z_}9B|H~hpAY<>WQhq0gXs9=OeWq%pn?~5It1VM}#LOT0+-DSvo;axfjhL5tKc8Y2b ztkvSc+6(73Dd<(or5_%{S}o2wL8Mno-~-q8D$sWqV8W9_7DCud9m@A`k#Cog2l@?3 zFhvJw=Qo48UdwC+t=zUs%Hs(Yh$pD?H9dwrAouht73VklNe3V<%A*Ei;KNcR(%#-K z+;YeVnZD1gqi4-Fg9pm{Es&1iwVA6sXVOBss)EQ&y*mG1jE9uoeBkPWo^Z#nW(<2M zmRjL&Z#o_Ne_%%`(+!x^18xO4&OT4WPDGKoT958Y>9Eq+6XJo|2vz=lCw<&NI7=*x z{irkL!VHhZ=3wR%df>C@=V{;a>4IJ#ID29K*`hdCd9I5@<73Xlj7dXiY193Neyv>N zsVDRgy?6mV3De~eKgxo>X^*CSmF({?Jm7{l@&yi|gn||AH%2uE|D}lrWEdrD{!a#i z*BuoWS|=5&B@-Qg0<+GOz!oBJW}nm-81WMkBr7FQtg$FjooQc_AR7#rR%@5|@Jpj*?ht^vKy01OxRwD&{0k z?tJDztb;X!55oXM7KACN&Ezfnm*fbOfM8+*68NE*KS&y87EuIDbWCIt{1fm*vYux{ zA(luB5fw+!m{ehiKv6=K(cOMuKRBHiB@-v5S<`|kye4Yi04}UKeKe$rcqA~$E|0gX zA6=g^LHxjjVci;X%xGJEt^l-dfgyPKSmH~jV$74~v~+?2Nb{c=Z4xvn3clFidou|Z zcfB;GiIDN&%jiUgWK7t6C7VdlB>Q6W4GK6&L@t6D53e!IaN7a%L1k|$Pbwk=tY4(_ zKen1MmB4l|5r8?{@ip1{*(x{-Cr@+=KxB9 zsmj+w4myU~mOy3=5F*|MU@Z^AtlsAjtD=ev6r``4(9xpmQJjoLL$aU|^M(#DqKcXg zJ2fki{W4rUG%;k4MnvJUGGluDD@B5D>MW9-zN!Z_PG(#oFF&)W2>M1Rzxk5hB;kmcZ@~2FYv; zj!L4NYG}o$@rPuAxG$mMp^q{bc?yTVmYT)@LDauVPH7_Ej?D9ttRzI`&(uD!xU>iv z$|b!uQ?%>Uq%;=w+gw4ihAeHpTVDfWjTDF*G)8q}e&ySQeN z?-OE*-H4mId9aI&cf0xqMkq3?+LwP082`u(+Q$2Z( z3i!VK61F9Q{SBgElp2UmZ75c!bu9vH!5$4;7DU0s?8wj?9F9V$KM+|8wTop&9`pS8 zhFI@)3zpE3WbUc}VgMb0KsV8SYA`R($4w3A7p zv2gf+b-dTRHp?x#uXUHOnan)HvUOMwTvmU5OB*E1nh>t!bx8)(((Goz(w2WnX*C>I zzl)wNhM_5WN$~~^6)9~v-g;v#K^LGa_#6wFOZle+OG1Qgf^c(2iw`)$gO3(CMr=)A zeC)tt|MgE@9Ti11{U5)rgT~c-lAx%e19Hdy-BgTG8fky5SI<$}z=7l2eWN3<`moP* zrB+9}%d0dnt}xNn{Nv%Ceh5g4!0)1(ky2b#VM?UUK{!XPs!^^yDld@GU@AyY?V3N3 z_^`09@bL-%ELihl5#phH14XJYeR1O*WC>ir5}A$Nq zBFmaPyHp9<_RC7fnOoV3MT5GjyrBe5Rbg zO2P|%(GqNx$0R{ujgQw^jnrO)L~E@tsjTf>n0zq?A$6ASW=UFK=~R>e8Bj={QwXW!r9{(aS&t z($Jx=c@hpfPNjG#a3!FDHZ%Hq8|r}i6)AgkKOo{$aCtdEOl=ABSZ5IQCoDo))XYIg zK$9Sc-W2gV=Y+zp%8Y$29i5Xy^o&2-q`v7bb$Kk$t~UK|%7R4f~0ioVl!l_pdG*2T5%O(p@j3c>IsJ{LKXC9PZP@4$_+LD#3TX zFN=siwvay!6R7i?`0o|IJPpX#dQlN?)hPbn&9y;$xgHKqqGGs$srck~mB($=A9SNr zc;KbX`u{X)B+J8BW4}~WJ(LQZV@e;uKLotyf!s^kJbs~~vt6xf!h=<9+;R2gM$8m%D2R87Ahc@{dN<$-E>5qm;H&*64`gi(P&P)A^W zU&9rGM1YaxqaY#+1ca7>?FT}Uum(a!Z6J}Pr-LDj3H^qW9!4Rdf`<{~8>JRQVumlI z@H~M+KAgcOiCBX-W)1cfS(HHH5yQXTL@Q+Q5Ac7e*nsX9>(4NY_uGCGOHoVZ^ zKo1?Hli-tbq>~_vWhA?Q4m?D`B3$8TA?#~778n3HfHA@?;vFx*2nm29fG23~B*85m-K$O29-3pn}af5Cy@A~NFx00n>wI3?{ap)m#k4!}tX z0S^Cj-VVSEfH7bbJAgz1NM8UT0ImRIUO*7V|4BeVumC&&0bT>Hc=Nxj!@NbThXQ~F za83wtiU?5r04T`-j1U1=mI?v@U_b!4wohchRZ#-GiUO3500@TcFpbG^Z5~CT6^=uEW^oI1O_C`_yM6A1w z0i^9F)Kq^Zevk-7>F1YwtdJFs30VcqMwprc0X4?fzpy~gFxcjvj+pEr8vK4M{QeO% z?r^v=w_5jElHCaLY4K?=P*BkbvP5!hlBM||khxgtQsGArDEHr3*BA%T2{H=LVd;B$ ztq_H5ZCq&keYZuj5h=`=t4KC&u^6uTNPp$fI8XCD8u@Wc|?ZOyf_D}?)39*4u2mXQp z01N<>IuJ?)Xb$x+P$A`E*`7!(&=d@$+Oou#o%NKws522f0dO z^cSB?@4sD`%075(YVck{52r<*FDuRJQibVL-E&rO8REg} z3?+JY01mb>A6WxxR{SMK2AW^HyH61Fjas)Qs5v*W=`MSb!@&E+na(pLGI?k2Mw`EJ ziTMtytPG(l<19Mr7%4rvMjrVps^}%z@52k_!|~4@b*v}vM5$bxkP2w`LH ze#4eW=X(RJy@4traY=Ap$Er_OW>yxqv+Fg53E5kJQ%;&+o_ob8Alh)iuQfhXp1H*m z)Q=6)4;r0Izh=|kSjYZBWYjMjO!@!gby5l{t8b!%+%SBnySo?FO9Ba5Hn6qfsl|kv zR5_TcKuX$9)bfy(6?+||DqkG2_{&^hKW~?}FASab=SymKPUG9l$(-=_WA%`6bY^+o zEahIqTB$3F#VL^c#Mu0I?v)w#m}LLmdglD|B7WPOeeqVOJc|!pk^vsOtg6)qF=ph3 zf0vEerH{M*FcUVTzUXkZ=SGv)_qG}I!@cwXTF9Eb6@Rg^AU!#{@e-?!k5h;GJGG9T zRoEWzN|Pb#89Hprzx-x3e0SC@x-GJD1b7;V1vZFz*jLD=T$xlw+pGvF{Dq0PU7H*u zs%ziAQZh&j=G;wDwEF8MPZ)^`>%4hW?W5bSV~cAF)4MoXWEa*Sl?#!mmR2 z{M+tgggdWwJHKOX-qmb8s@t^sI~mwO(s9_Eg07 zHMdsEj;#5kz@s|u3*di_#f7^UHsxf#Qgl7Fvyy`#?7^2PgbNWxt(Kl_vul=_8Ajyo z`~vBWT#-ge7^{pVKEXwew7XJw zobKvAh%G5%9-B+HxHj#dQHpP?d5}u4pIvr%sP5lDDLPZ@&a=qj`q4Wb0L4@{se+5? zZNgN0o-9xDG{SGamS*)o?N{1kzF`3u)x~$2TZIwcNo5xzDLpv{ixbadPHGMivK^@? zKNwnCFMmVH!M5p1bq8mQ)F&z)7h&zqnIj2e$=h7UV%;#qK;H))*Y*@`$s~e05?zfL z+`LZ)EmntTT>N7yw6}*kCv0LqztVKyJSMv>1Ft~%E}(n8uY5&o{k`%7VH`L*IrDtT z_yo|4E63Pg_n|8{JP#*Xm3ieLETCEtH`b^r*X=DeTUo5Xhi*{0=X!0)-UcZ@>mgUO zg>;9ld4`GkYf<|4Cxf}}ZTa1HqSnVQ{_>(6p{t@kzv^^|Epuyp7R0_2G0f8?2)uZ> z+bGPa!n#2u7ec&CEqqMZU4N&RJ=EL5-ap=j!OsSmcZG@C={o6G)*vfn;g%|=x4G)} z79Z+Lu>*DXk{|uUP*>@3Z7jKzsDqX9`UU<&McEa~0>mao*2G59f|^d)%M&%$#P66D z>?x~qn00A-n=G8HVCYxaSC^-A3;TU4tDPq+_s+jmkQYxgPMZZaPfyUof?N;%tU-(kC-n0Q$byv9?*>k^6m*?`hw=OTKj|o$X zm6N_)=wA(F;83I8!`tENO&l_s$a`PnQ!>q3mmFq2+n0=i$u-7#@Tzu=F=xfi*k~>- zFV7ThcF)kg9WI>I#&1^MtN1QX`iroP-j%?oFh#*xrE|_%t&`Af*aVk+qDmP#cGlXt zJ1YDV9IsJ=YF>v)l>IS^#dKd6jIuUxFxSFTVL&5e&AUBXmMzZ&H!7JHGROXLmthj< zB$*bTO?PiIv|HUYAG@WSo2XerL_UR$NRR{```V%F}pdMZSCQ!rASQa6;aqZ9Ms-=9ZcfMAoU6Tq z+)iE1t+ld@(lr`}UyQVK9<%Qgb~`wM4AX7WJ(?q&_8S)Pff(TAedIpmnt@NO4Q zt_>ZWrgNNg`;xD}9OUc(v$4Ik*ROp7wxMmvtytu;A3f!-vC(M1iWr>T?_&Pm=yshV z@Ij9`D4JS6#%#yDyy=*^V!SOqwY9gPRAvrCh{ikqdu5T9D zAn%}_GZyUi{tL3$4*9mn*0N75e;>IvtrpP6t= zik4<%ie+-Vs#^2IGurCYBZG%GrA9_xf8|ger}=at_ZmZYg;&12==w7y`|Zf|c{B=J zN@C3+w31~W>RUPvD;)KtvP@dwDc5tTV!TJKqjcUb=CSrW*wVmR^S4H=HrtnB+weh|lS60I=Gd;Nt1n|`6 z%W<-QNhT&rezrrFn?PzOg_H|XYfZKyPP3c}bI8(8tG;HP;|klaQ>JPq>ZIlEqTyUE zP*p9^?UlO(4wP)3#zGg#hZhkjGYcQWslKxsxhSjSW;LQ}#GZ31^StZutJ9YoPQNby z1j)QCGeHFqQl?ho+16IdG(4k+K6fW)C*b)bv){@EU9-jZw}ts|EG+cG_Xf_^4YX9J z>m}EZVYHVFG(=*f>0g&OAZj`>)f#;%nm3dd;;!kFu(S4_f1K|zUv?AlM<2qB=b_Im zss^XVT+VV4ByKOSJllL2WI=VvOOZOPXR7iLIAjvDoqE~cwN+d7orIpd)o3ehdVaH^ zpRms4mGHcvVa3B~8ZNa6u9p>v_PmT14!(5Br^og+$s`Z@q_&AapLF6stHpV~LU>Vx zEfH=#StjH}gSe5ci_g*Yk?Un#2WzD`+>)YmAKGo*tAqROxn^`$#}0lemh3i_mIypO z6yrhSTEr861?V1}aMzw_-p<%BQMl>2=aS>=l*?IjYgyRWjZP^S7ixc036$?&<>Ejx zZVVoMx>N{^ql@DQ9{t$MsMC7%vWaQEp42{@ip&m zbDUCWtZ-KqFO!@5)9SZ$S#&%$PrSG8Jaq|XRA(fV(D`->Wdz4I?kuEmT68u(eM}|C zus`YhL)D}jzv~=~SD;h+;hyA;2{!reu+Yt3-j0WoEUyj1Wx6(#z`;6FMXTEShe$7S}*%B zN0C@*?BOP-HHwK{rq&(vxj0n+YOrtDF;{`mTG)?$g7Y(TsHr6|Yh(75T5r}C7IRQG zdL|&>Yw~`dO>q+Z$iRZ(M%_{619=AMeIaY@DgbJ+Ecd&&_Z)YkB(q^hk+&CA~|K zl6K+Uw9;pEoQ{EO)Y)!#fUa$5R{LsU)TgXK*Yd7*70~iIbl-<-CN8b!1WV48gN^dB zT_i8O*gJ{!IJKZfNT10O<=OAg53@akDscnL^KKG7tjb|IotGp*%z{<>;qbd$!Ar_; z8)#GQ3Y&i%6|h0CDc3Iln0DC6RMC4~Y%;y;`s?POc#xSGphm8kD(*Yyd(|Yht90nt zrDt49{F%Pc#mSqueQ)-#&MzGYud!f|j4(3*3rS963@#&EXIXbO*%6M8s7B`#r}Aub z+SOb;eWyU&sqe_q8<1FhwRQXOODki9eL6$9m~00LuW&h2Q1?eu*I|pgo1#W_9`?s` zC&uUGsKJV23yWUqHQr_mbvdl*dDDT(iUsE7`)L17D94Y#m(3)b4h!5N+j<9CmUbmC ze$nOqF47u<`?oyL-|NoXwc0U`46iK(kkhgsR@ zOBH|3BawQvPGCt+gZ;1tx<0u)dqc;my^|yFA^5*luwK5JM{`PYHblV zx>mKSrv>9)v7L<1T_avf%yp(cV-Z_v`OR{2k|ElTg=*&&M(8?llX1VUv~f6Jp#s=* zElyUd- z1vY{aRG~KU+r9C`OnV%~t{R3hL`@TP59QEl>B39Q#-q@v`N;ys;m@;1S#jisW!(^GNOf#V^4zsh z$>Y*!1U|0hYvJgRK>i5&TtY+pbo2~~t5$p`blvL0o=N|joVCEan&`+4=?@|Jl1H{I zJfFo$rM#QZYE>7v)7(VFHR;6Q{hP+B>%j;O?C?tdC2jI{o+OslUJaE>>WxWP|6@{c z@%C31zu1~#P|Qn=Ms|MJ-570RgL2MdQxCu07+Ac7bx0}Gf>|>tCyQNAM-B$~Gdr7mh@|SBzcj1>F+6ccxOM&4{bI46 z=0Mmf`l|b2mEbc0BHK@g*J9-lk&>#%QTMGK0k_Qxt}yHHip|WkosnjI;w5~OrDX3n zY~^;b1FY@s5X8)UkqUfvBj<}f-_BwV*Y0bbaB*(PPf6Z)ZSLg?3=Y4f1RpGi-%Dyv z!zts-8)yXI%n?Yy__ zIAk|#6!F5yn>Sjc)`u^#!U`S!SbOg**1Ed+D2_sNkDz@d`P*;ekn_>0raGn$Bicfr zcMd;1MLYf9jq>jV4&8ZpPfvER z?91snCOXtY!b~lUutcV2OkDob9o#Ac%mHw3O5AQN}q`malP2!*Uni zR(1Da?AH5*-3tB}06jp$zr~YXCU#1UN3M>C67~Hq_=@kQ{dLEY{?YSwoq+>Z{nWd) zw2qq7?5!>XIUF}rI}5!(+G<=D^!)nh+jYk#ZCm?;&eZ9a?uK zQF@LnoNXvN-2J%YoM)m=PzBwx&cp2oPx2n_tm-t}Z2~2_w)S(QRin6@WH@6B?zWXb zW?dk!E;_(5&NnG1kbKp)cp<&Qz+3GZm0;Bx>FZyR9Bs`LTsI&92{*e6^DNdeIewq7!btZaHrjcDz<* z%x|=Vq(s2It%Ei4TS?JP$mZf6vl$8eV90sc+-i2YR$bg6rMkM>KCg zuLMg`y}-Uz%YfeSsM}mRKqtPJUcXFnY21TnU9qXAkfc#B1^Jh8mKGMwPRcfiN{7do z>C#GyL+g=wy=L7{sINL5&M(O+ETg{E&7j5gmQhpI;z(I99i>F@9+c{M!QXjKu50M? zmzkn7{rT2z*p+uL?Rj_Q*LoZctNq1{=;vvm7o}f8OH`$QCwW}F+;~oqPWeuY*eY*A zENv0N-J&{5@t*XJCS`|W>}qL$L;HJLfWh#96?x|CDqpfbUT8dQQ0O$vRHtZ!A|AN6 zaXRDX0k$ybE5+8)hhuTWmFZDjSvYip-2CcwVbuBRcXzDrbh>RXtp7;c)9I#3qnKO{ zdS66wDz0{ja@xgBc2i^~l7H3keQd_-aj#e=EZ%?krMY;pnM5Zi>XmD^YGx_YwFKtp zY=i3-{;~afA+3dS)_49d&Wk%Eb+wwr{aC&hpvRT<)A-b>vf#*=+^YAOZ!eth2U=8dRqqYfh!PP>XcTV}tX_ z)GVnZS_m$2PRU7l06rkhJuQy1f#N4id8bLRU|P2Vfg z|86EwKGqKF-y0t8v+5Jy#C2J9HCx_fUT9MMxLC!nIp z&O92eeEPml3Y9`@+_N-?T58E&FqGP3{xNB_kPy8}ZAC`Wv`|%!kKp_&5w-PqwGdpI z!TC!^eJDL=t`XE$2T}d@kW{kWs;Tt)gkV$GFINwas5jGwdgeF^jF@l8R^r`5N&{h9qnP_e{S@A9&3EcdWxLUz|-ybD41EV204 ze%Bsi`|`KnOV}x&Ndgz`OKSs>0pgx5<$ukvsw=>*HX8Y~;$PbWx%ykwMauU+D+m=u zDww-V`YuYmmMp%7zdT^=TDBCyX*VdV&TdfoP2p)O&XTNkS6bv=QA*nBftXvrG29pK zR4dtrJ+gI14mCzWzwCLGK99bbWFDBT5%JJEsC^yy4Xq71H*CMB&-N#sQJ%N9sys5r zyw_|$#gm)$<7FR(-;HM}NjC1t zFftFHf7)G4_$jmCOcBXOt;yZBZ`SvP*H^0+xt@BV@9QvY$K9<5yMH!dz1^ndcToLS zxQ044FnE_zROTX2RoZ&%d6Ks=LbtDa{EF<)lC=b}evl1j$a;~7zq|*;wwrVff zb!5%H{0;r!7Jp>TEv0puYl+5l_S7YW#EMxx#nn5B&W{5;2a1iCmU`y+^0SQ0Rlk@K zObfWguiKl~hHqzwDWT(N(uyl?%hp7Hn~*AM{lCp{UYGhx&^& zXV3)Wp>tT8tE*?!IPxGpxm8)Z)iBO!tHT^~)tDoOE3pZxx{2C7UJo;Am2UTXet%|< z;W|y%9GW>Rv$t~gqt+E|S?iSrb^aZ$^bPG@TH93SzFp&Sbc8^6XcJxdTdylSshsZI z)>ZWMUBRCn(|+=pj#L$2o|gAW9rwT~?d+8WZ)#0<-^LYR2=#J#{r&!$Crxx!)s<;! zpJ&>)rIBZ8Rn0nwkJ{0;|JQhpYwdIcl}!dcMjiD>RrFhput_$Vtn)K3e0w^uqu>hZ z{ncnmrkdLoZDsPc8UB3Bh7wN<;qCQbK6Pc@82W1~MR2dfRYOfd`F!E_%jHzbUJXLD zae{?pi>8#Mb)J(oV;!i1`0hETAw^$5h%fEFThw222S2ywhF&QqV&t9)$=~7G-_|8o z4H?MX+#?ai;&bAd+;wx^x(LlCA3~h98(nnm{V~Emi!MPQW7<4IXD``*C*ok3xA6O~ zqJH78S{dndj37?QXRm2{#WkO&s>_xsE@wKfTWRULVnsmFu0MGzi~KMT{5}=*(i?df zRW=#5c6VkQGRTO}N@RMWA|}|;EvHilHePw9&CpZYRquwwG>7YK%1YTyY5SG-E| zi4K_LYLd48x}da?Gx^ARJwH$1k=FF@OjpFXs1g*^R`zdM?}8ycS-&+LZ=torIL}3c z@%AT?pTXXhBo{40cV*K3#st4g7lQk@kyJDFo6(Iu%+NFnFW3SJYjnXPe|MUSlIbqW zh`+zkSq{qSdtu|K&M~1(h_)5&) zhA_l*ZqEf{7bVTO$lQt=*MoV4=TXL=Zn_d%XofwaB*v-KdtSKifV3kfMs?Y(!Gs_e zb+?1Vh|A|Cep1_e24a8839dBP0)tq(c$K$Dm{OE|EiDvZ?=Ni7^uOokqQuVU1xsuP zY|^_l_Z!)@lJS0;g!$xMw{x|K2JFV!9;90i0cM?**KO>nY0HZXn*2&#!$S9jwn@Y7 znSj01ek;hTyUduDr)gFAHmOmvJ+=tPQfb}8N{^`qPiJ{6{XIw*DqVxtJCn2ST+zAI zvPxx^HTJ#=w&2>iPcJ_ny5up?;HJ4MNWF)%xltibeD_>!x5B47g$s)Vm!}CMMSmPw zyVS9M)3t34OHw754XXGql{a+X9Izac?krlYcKwZ#l|bEMpk93jbDc7c;JW!iyjHzR z;LjFOi$#>okEb%f7wqZjZm_>*q^H%8yRrM8|AYFR=x0%>NZX4uPP+S7|AlcLxTp}Q zb!3^1N%s&{A)}ndu?9m#D z-rfXj{Xb4%m1(%xy-~3acxQrJ6u2UK8qf{*f-7H=KY5KMv;|W_sp0>o(B9*j{{A?C z@4lH~?!z$mrNu~^ySWYv39YEeuxcTV??M>Em@yJUk|ar)OK!Ofb18H~spO|}tK?hq z?dDgX$LH~QJs!{Z`SYB|%_Vp)n61Y^?^s%FPy z8JV||C74{7lDx1e9<|Kdvt#x|#d9g)6VE?i!A$YI;8&$*w)qg(9q9K1^$33Xihy}d zXHcGLKp1Wt@%^)LNXd8?1d8t)_-#APPfy4^h)(Jj)~HP)F=X*7e5ELKY1b3$3_-Bws;9%A zDIxJD51ZCYE*~V_JMdItX5K{H)Fy+b%_QB(m{b;R7tH}_zj*Rb&YQ$r*JQ?o*U4Uv zaG@Y;FjLG&de(A!Yp!MxQRkbu%!Oy>Rl^(Qbyj~T+KVyWF+sn{8>|~JU zoa_JWk-Y0#+mb*oMoM$cUI@~cy;?88j9g8Wf*~=Ri3fPzKIVD~8TR!?QuRJ{4xSYx z8L?!({4nY&Z&8#5*+wZSch_t49nM*GqAKU`0l34*E)JkIZru5TQ?a1uxYgQynwF#` zW3_8*v>sMxYto~2mE-gKp9Ze(@P`C-ZcdLY;W4AlhIOs@UC93>q zMU&q}oK^zQM@cMWnC7(Zp42T7i7Q1~h)5A$$zKc?XEY@2klYwkxt+&^wK_pN53i1C z=EFIcyAj74@#i_dHb)Uj5jG$hpe^l0ATiP8Xuu>jgBfm@qzS=IJeOV1AOy_wIi;(V zS&h2(2?4P;Da0KRau*Eqh6k*Fh>`m`qtuNm;H@f8BSc(i?!%S|7(38@G!PJ@cV_%l zhz$GLUd^aLLvp}9nIIdeBJ0%fdf$X6uI`F3rKx7MVBov7+&50IciM36eL@yTTPadW ztJl)J{Bu-N29olI}kZ)olkS`0;XLmkR@)aB04tEh;jBfJ(D>9|V>jU~lsZTiwf z$cU3zytbD3r*{NiUpL}rqjbM~QE7MZdX^YKfpNY=xwFj`cVk;F?aj=Eoh0bv;XtGrL;)D_Qr!I zAN&t+G~_s^ERVmzLi!3T!s034nxB>?ljr!&>eQ(hcOUMWi^rvyD#Gou96O`Hyv3MBs400Z%4?mGyb@dw{ z!vH!k$jpRl#4`&*LU$|HF2zVvJs}No9es*fWpX&X2%t~jBH>-*VCa7;f13t0Hi~O9$4Me@=j=^y2K;rd3@lj&P{wj zfhE3__=J8F4l8aZQk6<#ZV>ISCKw#TvyJSqF<2cPNvIW07cYqHx2kvU@l#E;S*h7R zPB8D$=9TX--F;HNI&ORq&@b_Yx7k9-bwY##8fJ9=z;()btR?)ZjZ^5VySX-l zhP1sIIK=~^(@Nyd9lcxodpMkXvF5Nse%}HF+}9%ac|lpf&K)HSYT+T9<44}eO5G$) zFH>w=fh4{Appt9RdDwO_wQ`*K&fD||0Q?WQr}4DX#2nZ_g3U_<()05=X})!B7kL1` z9p2z7fayr=X^e42S>rF&Uij@DQTCc5+>4P|v_u`srsjfcSj*sP8|aLo9deZ26raX! z={uegDf3w&fPIOX@5YL{#IzI707e*5#|2&YtpNvEYVD<2KpFr^Aj%tm-2R_bjC8oS zz}h@4wbN!-^$&5R(YgN#{I@vYzX7x|o@3(GV!Ql}PP%>8+ZS_KW@_U#@W;4fQ+Box zN#aXpy!_p;OYnS)D3dOG3a}T@qQKPKT`!Egp_=^CcXu0NDxl8mn;`3yR(P8$K*odl zW9(BA?a%R=Ws8a|CW?qG6QPt9?bk;9ae|1z(veA&w(JJ}JZIEqgqFF|8zFE4?2?n!2T(2 zqfpnqO{4SB$~ulCVg-GSaX~gp4{I^*ZEA{h(G6E`Uqc3oMF1G$%{i%OD-{>lDbdo} zPK3{X_RRG?7ibGOV(Kor1hfO^Q00a+?P}Ltt|$V*;G-puYDdJy27qRk?@WdX*Ea5` zxumYco8Jf%znu9AevsBxA^ZYA%rx8gm<6tE9-3J>B0l~2r}S4x+*ea+PWYJ2_I>|8FuyUD>^-Nf9E?qJ(p5qE;CCq{#i0?_Opbb+46#Zj8rr z>+5g}Ew9IWBbUZznl1~CLz~fI#b5e0~#jo;Ft9E49A;eKRu~y^9Iu0e;2mM+PAq;s_E|4t}f2* zf6~d3{wEyVy|Vu#*knLuqbkXVU<XXUuCrw>vQ^x1Y!ACUR@1^Lq zFCtY;GE9+wGJE&ppU5*SZ2yycO3nXT#ooVmomBhREb57Wt$Ej9Yq#nCYx@Td{xz}l zUwh#5=&$)sANp%^et+$a+q=K!wd(!X)&u_9*CXFYw05?Ww=XR!MjwD&bNXA7aQR!3 zc9s7-oPu%xYw8||G(z!@Ymcuz5d!hU&7z-DuzqGlknZNVqZ8`pPc7Zc@#^!XM&S}=C%Y4%8tCveBHD%t{ z#=r1?)?KxXo}m?#GQDm^@s6F4WN(h&UEb^E524v6W^$mcs{hfiZ(f+Uk2rS4keeo~ zm|1Q@_?S%=SGO^U_wKZG_!51?F8Ib~wPq){xve^~d(MIis`)nDyL8!f-hqdn3mMV@ z{|jR7Iqfx+#{AH=fsq_)GgzEuob^VXmv0H5aB)*mktg=bsBI*efw`gb*bSyEEXkwL z3B_)aPx#kZ;8j$mQl)ayHAz+8=eAKu*1-fv*v+D6!6LGQ!{RJ#F-R07Oq$reoP51K zr)7$|$HDmK4zm%nwj30@RAgQzZ{BA9mIDdDv1hq;rQktq{)dDp*`*|e)cY9S3G+2_ zoB#2&Ufm0Q4nAi!l6oz=%_mX)3aiaGI8wCp03nL3a?u)P z?v>FmrGIPWpIN=yHSK_7t&0|m2e+Y&@+QY6LbC2;k*gdC| zVi!t>-b7H31Inv^&`09E%Hr$tgfJ)!IJo1X{6<3SkRd}v*<_9VMxGG4sqQKx|KKKM zpd8B`?QMcJH!MUJRw64^wumO)JtLewc*zgBv2W(AzhvV7p8G9+Dhs?qi@}L-wf^?D z1|kF5G6iApsU=ijFG%^H@;=a6ddY|UqeKKtGBE6Ad1|XeiyPM>*moOx9Pvz(9|bXa z#IZHp`lU{bkz7^hSQjE$3c<#b5&omm>l<+rD0f^Tr+&y{6-R&i>1H5Pd-l$EKGGtR zrBtWtV~+Mvga&m=oT(|P3agZsbEwb`T7qskt_=xNFS69;w@bBn3s&$L#Hx(Y3R1+9 zs6}CKnSL=@C?qziR9Ij0rUd9b0BU1pzOCxGnsXFfy%t-=Vg=mNB+%3py1sTGIh#89 z-;+gzP)D+AM&%NSt~{q}?@|r;vKJ}L+p*IYslOT-Re1GZMRY53TmcPN;wNNRe9X>A!Obe!McjF! z=`m}*u*!D(tYj`f8Is&L4*{px>pt8wAde$2b+^xZCwRAkrl-mYW*$ufA`D@MB7NQO z(S){Ei1vnSRRR^g93Xnwhj01r2<~n>v%b&L?`B?l9{-2*A}?-8Ho2R?LL)*qj}F_# z!1=DeIOV+K7oDXB)lM3xKZ(U<*r)gJ^@cyPSZTelfdKG>e(ayh9Q=a4HaGBS-5E#6 zIWw_Qkb;P1MGOL_P})_I?u7N*zlNF8ffnzQARVIJZD1%|7ci*tJ=Q4kw5iV*l|m#W9)u<-}F!l8F=92 zoB4FI3@%TgD3_H)@wRaHmMPFwv7j{7TNCiJ#971QYBLI;$?2@Z1xbks%R{}F@9>TJ zJC&`!T5|W)!F^DJ_CvFE5s&5fceZy(N!1E@?T2zq#}sWqc^kH0S* z_2>4L(|2=t2(d6lf%*eDjRZH3l7?+e5J7{gTh*Gc_O9RhHdclL z2U3hwfiSZ_wU=bYSblP}MBMk^p9e%w%Ci(>Ldh-y87<(FZ>8!#$D^ZR?@(ye2?^r? zx>IlC7BG@eu-o%uF?r(0v;SC8S~_J4`)-}?(jdMV=`vRbE?~Z4ohjdnROQ$*psuPi zbr{9o+*xB*%S_9ajKcwD4(rfQ5U9@pmShFL>Ghq26WN#({6so9Z(MgbwRP7bxAl#K zStxsQ$nc}0@tzeRnl$Q+Whm9h>;H5F;s>DxRJO> zAi+$@rxniF3?G4pD<`bV8o*8r#!#F;U@W$(dI?VtKX%F^&ugkc_w4*WD%gLS5nw5r zQ8GZwGf?b^(@ zZiQXTwHt{#VI*)V4b3018|ok3-27;gT;k$&+bN;N21f6VawAc#L1LJr8Ke0OM(uXa z$qA~f!=_g1-FF*C42pn?+Q+WXvUlH?2pWvneLS9<$5%r1%i^1JOVq56B={iRq~eRLkck%KT-W!mkQq1%)w`nDf^q;cP&SPQ zdB>c+jezXwZ==V*Vk8*pLm{oOkB!+(!{=$h4-Ap1iv!b{Asyc&J>n{>qUieP_FzB2 z0Ha7!EBDqz%pA*0f5`0=dCg2Zp5g-%c zMG?8DQ@7jFZXH3$V?Sec+ig?3-W`>(#3IyD=1od`OGK_aR*_H~F&wk}PSns;aXr@D zB3k&oBkWojk2mPaR;Z82m|4AVJm|ipZi7^QS5Zix@GGCfbP|FuJ+ubmxaZvNCIJ0_ z!+)M+o75BqVc(cI2BQVee}24~POCvy4hg>oN<(B`X{TJfa!u5nllTu$wYVNKy6$&S9+Ar038mDxIXNKLb zd6}ZTn;36)8TkY^b`nzZNj6G(251}^%Pz)fjJsLa`Ha*aNW(E4_{;+|!TfdY0KXpb zQd-Q(tb{jIYay^W@P?dcP`&o`8=Deah_k?31vmpeHT?u6W0kuJ!~)9}aS!l#EYJ)X z5xpoR0!cvRx@mZslMZ3cKY1iwd7C>FeOSVSqNxlW$1|EP!oSVioLod57QNQ!+DlPD z=Y?zqrOlZlWj*|)K}=~Fui!;ux-hzNA5S2Wje?2BMtBG=xM&G<|IGub_<)tyRZX&y zV$GOF&D)3^;Z%rh7a_)@*!yyXj21>4SLVQC)WU8vbe$+}@~XCo$OPZyfc^bvodE}F zk?j1!_6=U`I(;@M&+H zxFYI7?dAyd{CTWZw|Q7eujC}T5B|ZhScyK`lj<2t=}beNP-&ZE4!qDkAI-w4&EFSw%H6HG?LP zVWA0qZH?oM{w0)?_IF#C)f_J?z$Rxw>-eP`btRJFBYVN))<$s5p&z%ox<(~J<8P&1 z(MGEszCYxrn4{&}KbYU(_|Ey~HaAns)o?OV4wbIw+&Vu+Uiv2VYftiV8J_g*{a;N1D?RPwXf4sFEzPi0it;7Ab|lUOzVPj6D+cg?;PHQ*HejpmN2j?ZFC=+DOs z624sPODPw8dqY8_-}Qcy_g8P$q>>=Q7)W1c&<@-2Yu>w1B>{H-5W$nBPWyY(hbmI{ z4}_=Q#h;-y}MYl zN5+e;ervy)1`asDIorGNYOwhyAO#}r!(p*BuU(=sHquBkz&LNKEgtenxw|gtU*cx+ zH?VkE7O+GJr~r@s?7H?g&7?r#!gUJPtG5RCqQ(lVP52!6lu`0d(bKIkqhbfj)lL5r z>IR=*s;VpGpUb$M^a(tJBWgk0_!m2um;s8FN0pnEX`PKs;EWACKF?UxrVQZ?12{vR zOREjx?VBdaOb^|$OJy5rz-~jsr9448`fKsvWPeQKR7~8&6VKCm_qEnl z4q2ZTr>pfN8aT7|x?l-r@B#B)e>uX%NQ-tqzE?#H6UMI$4qSHH?psLxpmI5w6C@)L znF04zHZh|5iy>fJFZ5?2*U~YqiCuPD*E)Uw#7mDAC2h>|Gt+}C+np(T1xRI9Cdeno zJG0o&$v^mN{MpM?V`3SCLe{sBhwv)hlafQ!ukQq9TW3b7 z2m8Xa6pMApC5Ou!5P0}^oO=3HiY|NNU-uP&>#{SbH2M_Rch@^iW=JpB`0SFwYIR(L zND%eDnWz(P^lBstgebup)i8@r1D)rsiBe0*@_A0-cvbt}@Ukgxe zZpR}bh8tzbmbZmzzN1^E^+NHXGif=OK;1QROHRV|Va9$Z!-_k5X=P#@F-20>hzGFWbIx4z*k%II_nhb<>3p&2m*b2$YO zP<5SD<+a!KIt()2zhr{g76A>25G5t>}TPXlat?;1u5gZDE)65)zukBP`ORpnaLn?2gC1e4bk|Y|j zvfouIeNoaxE!)Fd_APac*{5d~Yj)}oPrf>cxK53JL^WTa;}|+J4U#5I;AkL~ER4YI zV9kvl4{hryVYv-U3@6P&Z_INP@~Y8E8Hi&*t%8pqxKU|92h5nzt0Hfrh>zrFfH#=*<6Oy>TE+CIhPwn`8Ruz7m>hnTv{bR64$awOy@V^zENnUUrgVUq-SdAzj$FAl5P4((Lkj44-kkI?=}Sjfxfv}a5r&k=cgu_R5J#T~P8)5+!Q`{tngRG7hshQ;?%k2w$RcQ-?6PUxZ1mWas}PTmfyFbHsmN2A*0C z6+fk+|51BcjFVN*jDPj!)pLPhL4NEdzHfWg%h>)kp9ua+IZ`z~q1@QY%9+%czf&d~q~%%TVf*)W8+fhMg{uX9BBdu&eM2o@uSWcU8y zq)x|L2$L%yPgKwO`Q;6GX1V%%+xU`7c)onW*NoJ`^bj+Dk$B=n#~t#978Q=C_?{@K z)^)hn$AcozYT0>-{KAc55e^9|)T z`!k>6l1Mblaklq-U_D1s?gH67aOZLEkx&{)i=EM5#wGb3QLDEs;($euz~_T}cPoaq z2`>2WLs%NL>a1iML@{4AFaK`Fpw*pkR!pU*~* zC@P}sJnmWde$!j9=ZUFXIx(?(Yj$L#vvSsyBYT%ZKqsphcYle!n1N!{JG;^}*}DbM zz7RiYDNZ7^;rb_}!@yYc8mWM-5-^(2L6xFkpgu*~Lr>g1Bz3B}W94u71$q4C3~;M$S@z}d+i6?7D?*ELdi85uu+ts+f|@z=u}CoeX0~;$Yt=1&xM%rXUG$a6 z1%j#ha?o3=*p}B?&n1V=-XfIqnO@Qu@TmMRl;;dlLRt_73=wz`QHRW|gI({YKDx?N z?upVf1{VK1v~zFYNA%8S<4fnAiE1xk)y+AcoBu-=GykmiZ})jo{*Zc&#(3jIRMRB? zA{v|&{Mc51qLPuo1bqXQct#UWb3qB9qS zHeeg;3*K)_sxS+#5*B%t(~7$S>Q-lxAX62*I!P&&#-E42>yKBetJSV zYj2fG&ynj6V4ZO%!t`jAp-bDP>%}-)0eI*5M20n0cQIRcKId_JrFG_M`6ZQeTd%UC zrPhoDzP+QBN6_n_H*ON=f-3%Pi`Nw@o;d)T=mS3@S_(A#~1|g zReSKJXN}`cEs89xW0<~118O=osga(2_Vf&*j+BGDY6=_4^%@|h#a_!ad|cETA)`6m z>Tbu>S~fYQ(`Q?nB-um-b+J>Ph7^3(bBShN>~kgfu01F-eb_dv4>;7{rED5DaRBA{ z?R!jwc;>bS^KlCPGBd5us`usPC`5}}%DfiILzUYWlyLFrD2+ zGer?VXXv|RFnGI+-l@^K#p$z*^gOewq+R`zo9<#OrLyDp?7Ry^hIRc@xreW8+IeHi z@OAvQF{XmZh3TiaHS~~|P|7HZSdkOwTImUEA9^2yOs2|xnu&w8-zRE0C_rc26OU1N zk)Z!_77=WUf8lCE0;DBYeu7BpafDpOVkoYOZ#GLx7c0jS^}apb*!*fGPV!QMT+oxTavld1Dr z{!^06eW@c^U)??{%N7jMj#1Meb0BXqE1rNme!Jb^Nr&Wr-w z=vI#WAasNY?e1hXW)rSJMjk`gVFRL!-JA2#Sr_}q*k4jtpksH{XFl!TE3eEs59rty z6LKx)dkaNFsqa2pi;qp$)PI(M)F2Qw_UWIUUnX~*KP(HA~v@_a$# z{Ssd|0K!$7W7?Nh`|YKW_b(4KguP-GTP6p3`cBH7#7hY=s%53hm8c`A$;vIoB1r2) z;l~v{vO4K5JLbu7J0{ysrl;Wi{abFs!d#<9nQS|D&tj?|+ukLLFy67RgnX|W*YQ2K ziXxU;j?2mHa*PI_@V!`6%#vf=@Get{ds)H>zVu-PY9D#O(Nc1c%soRj6NC+n5!WRO zlHa{m0!)(r6EZ*F7awYVke6G8ETjH~Yi&2rIQlYZ7 z-7pr(dEVuuf2m@yo&_(|ud?RU!*-QQ!~u1i-BDtiqhj$!an}YXy!fApU_@sN?=D&? z;8_+42&;Fp88|W8z(`TWCWC)^|L(5j#})Dpo#1rJ8_kEANsXXD;+@mb&I?k<=#7}kAnxhaa zY3}QlAgQ~OhsGZ_vO`-v@I|ryrm+>r%G1Si1rkSrRn)0{m|&%f@8{_rGuN_Wb!A$) z6YsJ(8usSAy2beLc$jjTloNjhx`|1rX%*;CUL7ukK7LX-<&KP83Le)VpMkC{D7eL% zVXoW-z$b`TtfcvAzeiwcXK+59KGsbtgO)Vgdt;GjENG|iEFieY+BBGpa%iawGwi}{ zk`{*bFkx>(NAd`d2M_4qI(8CuC+|_Rr4H+L=CgMA>-3+7Hzuof9tzyyCtvhB<;9R> zoUYHmZHY|{eBRk!5nRUb_I$n@&wD?@?wj{}NCLB1)sVyUpGM|nGUZvkp9`Qp>yg)8 zkkhMjJXq$InP9g2)a^ipQN^RydNC`K2i4V1$3J2%z`b@bpF4j65FVKG3Si`~x~Txc z;N~;*GjM!75`S3W3JQp+nvN>@U3>F>g)p0uE(1Sq_%yxeY$S!q5%=&uTgJUc;STLU zKag4TPBp(vu*8lSw1@VqBKGl0ql0AU)pzRxuF-mB1&3b_0Mzn7nQ9ZyV1_UR>CYyF zxn6?LQKliR6z>OFe7uw|AYO%JG`nDl*@iO)XZ+nBc97cln_s~CsHUaGm}g*JrsM=^ zX`pDaR$3ZD#{x3;rx}x~>&VEc0W~_`j7igjBO0FH0pV0Y0lL7;?mCM=|G+CgKFfwD zkDgt9t+GRnlB)emF@v4V$$a8MxXGwGxPorcFism(vgc3eqXMP&BKKLOEevJ=E=|o< zG(+J0cLRvlzJfm22Zb0SQ#k0MA$RP2@hVC>8%eQ~Z+m_jVd=A==iIcF&3L2}8N(_E zTwDsk-)|ibV=cYwN3p z=!~b|lt#4UB-5s3>r5+k3Jp9a3+w`6R0<~T8qtH~M3g|9n=19?`SJnYz>I}+s3O{X ziv2Y1lCLtOQ0_yCJe2f;`BL*}vYvaF#I@PL0EI&>TvkF4h7md*^A@PS2MY6n$zsS!hr9a=DX~FH z!2gqU=J8NPZ5*FF_s+eu-OE@)DZ?0)Z3bCNg|UpRp%7WJMAj^&^kxlNiZ-I&Qj*FR z*^)gPDU{L(QB;JYRi)+~-t)&i=kxr&=Q-zj?)lt%XXcM{YK$8**`u&;{v6KSbc?3K zI<-f=xGk>o8S$TOwgB*|-7=;`f4kTe3>Mc)HBaod=Td8frV)kg|DI2@d`!1Zt4vJM z0lC)JiB9p4*;UCGw&otE-9M2UcGL@V&_#B%tas|+?+JME#GxYbQjv^ZPVU)&x02Rm zqS1FOKXrI7eYFWBa_;T9tyXWPvmL99@mfhWt;Rcn+)&*MUW7x4!x?Lp`c{>~D!^=( z=Vj}ZWe@q`%D(-GX70wdDeMgObFOetiKwK9d^mGEa>Eb&?f()N^<5lr6(TL+&6Vk# zD`#-5c=l?*koG9=yU01uDANoFDAgbVlHDtYlyGtGbc^dsnfR>%Gi@ToRmlpyxf0cS zOhvVT!K--_%o#Z5o4nc8Dac%hD2e2;n&dOpHydyqXsWK6vzqbcn;@&xeIxxASIp6Q zz{M{ERmy!Qr=6+z7}xD2SB1hIqutgdQ}HXbgZ4La+jvSwS#m5>yKo8BXq60Q-}U=D z=!uU93^jH&;U5+4GiYs3tIZk=W>qTgmo8HkdGdCLOrc!L9h1duYtu~Y$laEP8d)=< z$Tn?D!?mvka#lpvuKu>cvuW*|W9q-{rIL>J)ITlmK7QxthzuG~N-uf6#!8FCn5G{x zs;13fh&q~KYm48D5AgN!PzHLcBHTY-J#(CFp(C5(s?(ko6^!A+3UsVCBkoaVZ8^cZ z6!PQaJ-!yZ3W?8>pRKO7yu+;3Tknfjw`s5Lv7UQ3bGhvbnY${k#P9fI?nQt^{IcmcmoUv#I+uxIONR<@Uen&&~)Ec4djt zf@TCvh}~BbNd}_VFqNf&ElPs2A8R!i1ls|Fg}BvO66~kHTkoF<#v5E+to^olXezQX zg1B((z1v-HvxPm%4ETXIB|CJRX1*m}|HMPV$zTHPYvf)Ym}PX7Fuz*UJ<5d0Lkk8I z3nn`2a=aMNsOM=VYK&C1czXYpRAEPPu~o$!bKUw3F;) z`sxDpSyZYFrG#Ejc}9Jv$Wt-P4rMudZJv^1HKm^kU8Lv?$2~I){IBDXzTvR6k?q%_ z!^0}3!}?rp(V}T7t~H9NNnBnNQ0)^V=RUP#JbAc4hN{)xO04*GbeNee9`#_t=J~8L zGnry=@!~H=!o>yUf0p;f@dm%tRbPmX82rMUtE(z32@T#tRfm~*&QO-%oe4#JQ8hP= z6&ix7i@WeM^PKAbqe25!s2(K<<%``b%*p!`E6kOVW#+GH)zTNyB1p=uwL|tSg_ff_ zmVLS*gQ97m*^9-?D;uVcfqGur86uz7Ps{fc{$rTEig@VYklF*Hs3Af6M5kAO=qx!s zsh!r}_JDU_LueZeJu1n$DpE+@55-qJ+Ci8CV-hMFJ|9n7Q~oL~tHmXq#m|mgFTNj_ z(4HpnuMAEbmdFlldrJVG_v4jK89 z)7E|=Oh$^x;Y0wAmLdlaLW)95fVG4MA(5AT6e@Zguce6TG>t<~Gw%+`G*^3@B#gt0 z%%*cYOw~Lto9r}y8jX?tw~5+-P7?nei|`K&JZ8nGd37EygQa(eY9rZ+Wc*7 z;*f0i%w5~2CaKRFqPs->;Rh{gW9T^uKXY;4Ht{Hpw?RLhk0y1|*QkgxN4&(|+V9DH z7Dlp%rk@%WG)$fjA&W}O#{7QFbp4gos&KtMD8s}1wP8IJoOS>gOOIGMI+L(D<^ctg zQ$4N`@ZIBy&SwY7jqBg~sw7xcFU%}FOx5oh3*T_npCCdXGU4&N)A+Eon(1d^^?@tH zWlLG9TzDT%N0BHtCc)Svv0H_VulN|^+@PIm#3?usGdAeCs$kTmE7s!ZCstNC#py`0 zbqd&eEb3|Ej?RvcT$xkrCpN}P{5Q^E;=onqlS%Vll^&~NqmwV^jgWCo>VgTy8sHvo zm4<`$Wb0L$<~VKXp{_Kmt1v@_ZDIcKT>U%X=K^b>8aynRk`(URQoup0^ThoovlG zIev^2Fxee0wSJPG+mPx`CrtE*q46Tq8j)crD~#}Ve9wCgZ8SR!H4-UW_biO?W`5h7 zE5uW4dNWeqm}jNz(z2+v(<2^+k11-J9uq@#V`aXOrRC5x-obd}kd%~S)kR+lssgyM zKrMN3eZy&Aliq{*=Pp%G4Q&GV*a$3YP3LS-_#D4*ocUxMrsA1sgH_4Aj5 zjYJhK2P>&u4px8Pe_2XS%U%wachO}zm~X&xu<)3pTEeAlH|2^8C3_e5cp6*LutJ;s zYQH8EhN(-_!4rlMp>Y5Zw3kGT1(RZSUa6f$cb-rgImvWgRa6~Kv&A*I+ri!4-Q793gy8PZ zL4&)yO9DXx!QEW~1h<2`yIsEb`#(Y3`QUA1P-bnpFqN!&TXGtRJ2o2Q*m&t^ew zf;Di4rJ6{OQV{YLCv*W59twA6l25fip=NU~YirA6)nRo`kl(#xTp(MKy@-1}t@x9-Hhc z8Lcb9^yB_G6aER!Z1IGbMI~ppp8uiRj|i;#n%}xRN%XNn0O%9Q$CnD-oK#;db`(25w(y7WW}R3OW;9oAh@i!3EFVn4G;G&hqg z^Z-*)E}M!<3-G*p^@^O(zPwJZbdaxjznlqdd@G4*i%;E!O(TsN6^{ZxiyF`#RRJB$ zAnr0sVgXe(`>d7UuDFAeIH6D8TJ2;X>-oKrf4m%Dccp0C4dW4LbmYn_oXxN7D*b$| z7Eskvv)x|-=^ZbwmMWBTf*LXFpaIE9wN zm()L*!~#xlq@L}v$aUPVLtc*7tb?Xrg4 z?T03^jxB(5I-45koL59_*A@YBT@6gg~(m`%z>n^YJH=oI~ z>}CRX@9;)DPA%={WX)5b{p8$90IN3_{IxX(N~hu<$wctc4r}LEIk;<2&Fy$V!}<~0 z<@kr$g~dKN(7)O#B}1jofT8#h&*v7QulqoH$u4`Oi=Qc8m3AWk{)*=%b>Y!%^s?$a zt?t+@1%;|H##>*?$n{j!v0Xt>NPBs0)S^OkwQx zkTbf>ZWpj#2)eGYHuFWb9nJpX{VAxr>G9$GZ-pntYd5P}?_>)5)TKh@O`twh8}UB% zvGTMoUT$xAgQ=1u=2YYxx}RLc(DUrNuscCy^t3?L5!KJmDJ6#NS`lq|(zj#R9Brvv zjdzx=3I}31w5qlAU)o+dhr>?fWjw+P^BlpAqe4Db0!b2)Tk;w|&JllO6jz4Lq(sXj z3kzZBsTakFRUitVM@NofVdV=|>->qMbvZAOA~gNWOxU}r#5bN1-TwE*fVzvGShj4X z;7M?=V7@dmJ=Klkme)OQ_redgL)fLqIpG3WEbHpqGrj9Q7yg&e#x!ua^8HI2-JKQv zkaLE*<0TgNTQ??Y@*;paq_CGW=PpJVbNxy&wIJ$uziuB5A>F;fhuftktTFbpocvJi z@8E|Dey~3q$7;v3myos(PnUd=<9aRr$Dhd&*sTP&w-vwpSogLcQ$xz|{RY!kQ~(*| z4;K!Z(uoyLPdTnSWJ!c)TfN!^7Fg#~(RH<`WJUC=BIoSo=>YW(o5h+xNrJ=2aiwSl z%gNPGuKmlR{U?Q-+|4~D71B|-0}J)XIF?vet*i-5P5U!0S!?IjcVCf)Ko83WO(G|$ zfASLlDz%8e=jKmnj}dHKUdX2T;^HaU4w|5+bl{J@`KANPY#(`a^*9n0M|Vrj#%e_6 zRO8#R;J+Cad&T?CKaR8KsfFg@NjKLdZ20i47@BFX9Dm?DH5}eLwK^sSEbVd$tqL$= zTmT6oSBt?;e^k}~vIr;W_^eQOzC)__{R}XA`*$^$yN>XJ&cchYMx2sMO&}`TP{O(0 zn|wc9yp!$otPmv_X(amWFOiT3*su0$eYet~Ywne^0G+W2vIE?7Kc#B*4}KCv=*)g- zUKRu#isvOKFt4&~=6=I2hp0|s3e;)(OT4q#Ez=6M6=?%n22-wlon1?Tyd`l4|G*-f z%jT-lMYrVshom@$-(A*7c@+Voiu1J}57>mY)Qv8Wek0DU=W$H#p?yVS(R5pBp995J zA_SsY)X<+@4{94+2ht`T#cozL`){S%QD< zlsc)mWevz0UMHdZDnYj_+f~^NhHE;w;+kkaD zx?>xnJ2n7b{v66ouX z?YWbi9CXZp>7R_P{L7fjn>}nK^~t$g37Ri-Ho4lqoJLSiYmmjHQz{!{9cQ--l+CU| z$T9iL%h_W1i)g?<(zdonPr^RT#eH%koXkMqxd-#oVGC(M@=6GjilE2Xeq@5wKdM24 zIfWIvb@gg+cUL~sRPWnzhGn{;vWGjWiky+EfBRsS=3iu&b@9a2<19KC-T-(@Bzsug zS*&^1(s!x3E^QAsW02`ez$vPc#@kqtCegnn8quffgT@_&=79GLTN{2-LGZXxlKJm6 zAS>xr@XLpK!T`p!^yoM3l#>Zt!o|Q4z_*^ivzz*%tg?cF`pDncZuxpA3=NTeFtYDn zy~a3h3%#5RS=WbIxuPBIo#=Haty{n0sWjA;lfHhCd%0T{!im`IbQ3RMMzVeMY*|k* zQC07?mk11hZ)08RW3>%P-Rzo#?>rVUlKB9&V1<{QlN9+>OtFRqC1-UJdW-8uE)H4i zshY!MshV_m&b|mmz@~}z6_@l~fyX0gd}LwIg#q|@UYVpGy3`a*91QcU9LbjZ;M)qX zm|~no@ec*1)Cy3Y`(C1fGsGCekPFq5xSN|O#Zg(H`_)$eZAtO!cjlG#z^4f5>VN-c zG*C%?F{2usb@HB?hP1wt8<}`8uO;mI`@hH9ui1Y%v$`nq|OWZU<7Fnb(#gh2P;oKy@5u}Z~cAjj88d^FTq7x+FiAFJ}(V|eA zkTT-qXXDzANnI9!;M?l4^rGH<)9E*}2;c`I=AhRFuwrPFp6+z`U5(3{rs@Gl_Z}sJ zjc5BCco)n!&=S=a?}y8asIuyp64RUF%{GDk zac#cOa3;L*DD`^wO-GiRMOD-8Wz1FM>6EwrdrZw14rS&Px5NwFdXDCO=L*APvxE;X zk1k^Z$g~&+daA>D_xEWoclWp&O}KxFYY2uFo)@f-mW?v2OH$Zrc)1qieFc`zFnKp^ z&K5g`CsKAy(`lJ;4Y)Is?d^_s!7W4omWI3qspaO98{&st8LbcH>sZTKw9haJ5Ghfj zvIGwSWC>K?9|g`3H|rZ*_~@EF{=`L9Z2;RX7oPNU=!ZG0Vq66CChi1{udLcgLjIlJ zVzizJ5nr#Ix76=_=NMhd4w0K~WXO$!{F=~abXIF&&=024{CjBl>ltsS0@rdSen?xz z!3bBJYYgOHK=APN7hdsA4)Q|&B?sWS9AP1oSLzuZz~X^vr*oI2_3C=CW&db4!4)!4 ze}5xV84!=-SmQ`*!`3CBaSK6|k@$D%$JdY9edkL!BU#?ZHq`O8xevIGRUoO?k^(AA z1@HQ1mb%C2nTms)wYahrnd*;gQ8bAjIa1lFG=e~w_1R>6wX^lz9yAim9!aEr*sC=72Lm=gce3m0x>drA z&~t09(vR0l4^CJclH}Ilsv92!{RTnwnvn%?DcwCi${ikXMO-S~uQyCYjmSEJH;@lX zgp0MP2_4`e?T$>>29HlLHrCd^XE2&nujn5*6%^7G%y2zi6ES|09%M$P=P%GTL?2S zvEZCwb1`IRY-=O4+#pm4GMoz6wq5;Q~5yBPQ31jJ(KcP9$MwZ$MX8T1;wV}>mP2n$j_ zm=9VF8WI>j$Q>3+BsTM<5e`nw2ofJMEMC}`7YQ!t1q4khHaT6WWo_6GQ1gJ}f_Dh2 zE!==-`#seVNCo``CRC#E*R6#0D+3j}+*`@-8gaTR>qHNd=YEFoU3HFP;nkRjgeUv1 zQH3Yf1~VxC91dx!u(M6m#4mLBX5e*T=X-yKrR#d>WL8x(R7Pr};53{|P*)Qk;#T3d zP5ulW)4fxJtuhO5?QetEZXhfo{1;FezW~Esmt7>IgFHWA8?LYGz^Fu2MdI6Y9Mq-{z2TP-w|hj>kQgxfGeW^r)Zr$7$Nfe`}d42f`5$G>D*r?>tzb3tJMz~Pq?*u z2t!%!G1AS|wq^PVTf)iUn=y8`Jwu?@#L?e9Rn@`-5tUl(!qr1aF;mR|;;`p5)e)o^ zJUzlN+K=qwD1j1c5q>~?l$L}-#0MP$og0^B8IAB}geDNHvxe!mkU&E)`6e=sd#`K4e8!E3%LDu_(CM-zrcNVzUd6tk(H zlQ9-YNU`qw0Rqn@Z3y#gmMQ!+*6B8EAL=7=FCl1i+NC72uuG_y0FI70p)%%&4uX1>RO8kFx`gS$z6rG^1{x65{9Z1iJG2$_Ggty2#;eq`cGo-rIj{1d& zRg$3XyBSek0?ZyFswC`7hGZIahAr-07~Qba(8Z)CA~_n}kX#riiss(c5!XXaQ+`+C zYB8!KuSAVHBzowoB-cw84ak3F2|X(o`-(ie5%ZL)J#eM)9T8d~adNCn-*@^2aYxc8|sf&&2dlCS-naNf{$3i3zWsSO^L|p6=Eu&-8VT0+-Vf3mLy`KrxmB3?kSYV2nljidd4Y+t#;6`^>TS_wAkwSJG@v7CI^(xDsbk@Kuu|#2BHMu+=7cJh?f{y%IrH!6Lvi4C?vUXN0icR?q%1!C}#0wh6 z5z*h8zDkbfBIJwgext$@doPZpts%>*>E)u-<1+^@*zXwr7+afc_Hjo=i}MrbFs zX!@Y8SB+>C!(DPxm!1wwP8sp1*u5GhZFof;wi?>P6s@gJB{hm!_D~|NE~1iSNK(7b z(Ovi+VLj-s@aK}b7K&fUPxFu#tPo=@=o3lq`ghhP&D1zy$v<_FT10+bzoAG@VcRf> zq&(^~Dgi^XclG4+5Pso~eM2)2OOHtAVV&Y}_05%tJi)GY^7Gh!5!q%J>Tajdw!zB3 z*bMCn{|tx}uhtXHBjrf{s^_1F%aJ{+hjoX)w+MIX=_-z2;+;o)i9a6*ENrhUsN`&l zY%}dv11de<6M3O9wnf|2*P5-IQYb|PkgSfd47o4!>bNeLXP7>p=43*?;5J1C5UozI z41QU*Si<$>Z%Q+&@3S`tew8^D%w%sz;xq%A6W<$6q#A{0)!VVVd9EXw_tp%zoWe~c z2cWG^ISy1WJ+Rw(Vp;Y(4%sg=Gscp%<1|<`n+YzRWkR<@yL}Vw$z94@Qt`xVig{BT zu@3aIykXmMoJe~^Se-oYU0X_E_qxZjS8aC|YR9`VeK5;E4V;L1!@jW;?Z5vG&qjKG z(@ofiBI_qkRN7g3Wg|ImkbWP{qeN5`*M>~JVz+|tkbD3AzDbsDmc}}z(W~*H5w7Sy zm`6INJfL(EAq>+Yok#XX>LoV3?w}s}RC+rXS@+8eGVbtuG<99K+34woF47Bp`rvz1 z-Z$2BysqfonZUflH`G!@*AI*Ec-+VgfoQ>&C_$)6Fnf^mXR{kXK5U`s#a(0Wgi!0JMq7M3;Cb*mbAUbscYIg`5iYulJ>WxbcW2+RT z!x)U34Fi4&Zh=}feD*TWR&S^7x-n&wv3y0pzsGhkFwP!-%v$}kh~!YA-#Ead1TJq? zyjlX0PY+}x>+~a?ySI{A(4XE_O*aIAZhKb1y?<_@3N}TlLDL2|!a$!s3$>u)@fpi$ zMpUhe5ldvdxRI)VnB~%Tjhu$35EGqNQICaKKxfVu+d{$y{gqcgap; z*!L$3{a@;~lPqcj^TJ<8bT4<0X|m(I5=jr<>Ujxh*<>@EQ>;OtUnHWBfm2B@{K8Fv z5lxFfSzGr`kz!*VIW~zj5aW97O3w!UV*>(=ki8ZZr2Zy?x>&xV%sBKSR7ClSd3x-Y z^+|C_s(F4~nvW(U$n#ssB99kEoFlo8@mR(WF}P5R4j!JwNEU~dKsAtx$_Ld~9I-04JR5cUC3a z&_DP%m`Clq7749&Q`&o?+yPWaK@{bc1;ST z$&ZoLjCf^_y7%UZM&I`*KzFeGxfdeEl~umXd0Y?v2C0DjUhN4e#;Emf`$GY49x1xw z91E5*`l-_maFdN7buTsFGWS#zKJ-s6wsu5I+Vm_=3E~ISw#~o5XUw=*#q9;UDVSYw zXFgbL;|4!d0Vkf04j}R6q~taV$aT*Khl`<{%tV-cK`JF<=q+1DKN$SfIVRT=`b@u_ zaj#CXKp<(TORX|)Manz`Q&^_iuU-)HEL|6XV4_c@B9{?GR&KNX8Uh{(iD57Hkb5iU zQvOxQtFNhr%Dxv3`)XZBP^rrJ`6W_kR1b}-Bf>h`Tu$FO(B<252r(w-;Gz!)z66e@ zj>N5xabNfsjIFbdQJ3k8idETME>Q?zK>MY>qg?M zbX)Y0NSGa|fdK|SI$7C|++WmSmg;+>fV?t#VOn`|p~{4TiZqpF{}!u$FZRc}WAfr@ z9NgVk8WZF4_x)$&epV0Mirp`)igE!ne+a~4ar=#$8p--5UbUKp5i);*G`NK+$bT^r zTkPh0ZCHxY2L6=W%)~QNBhKU*$4jf{%=-7|hm8Bzjz^kS4svMqL1L;c_C@zq(0B!@ zCXmur2*a-7M)l|SL^`}1g|1EBbww0%_V*vrcIW}`B7dN|Ke_9E1f2$Ly=VFHK-;9O zC2IZ-D2l8)uj?ikHQcpV+P(QXLhGhZ>TFlCgMQu{qDoZIvP-Qk<89QqEGpYOSnrW>PNT1zw%T>5KX(UQO*PBU#t(Sb9F#hCYz-V%A^vo48&m z!03qm>)ADxVYf(VeJR%7!P4egQ}=oIT!1 zvk5gfmqI~S<6~xx)K$7PAxJ1$;&@EbijVKgmFTHF3<$!+jejV7c1g2>F(%oMJuwt& zCV}GE*rBUUzBh2p(y##~S7=06ZZ2^|aX6Zn=-i0ArtbbCSz3!PUYT>`mTTeSme{;n zHPB&g^GFg{d}#tX2`VZVSN@&cN8Sw9RM}8Rp>sVxIT_?D#b1ql(F7-Ptc-!M8)Z}9 z=i#%z2iS&55~De|oUs+bc!m8yJ&6UMrpe|OyX3$AE@MlU!tBL0-)0^qo{bM^TPQhU zqT@om@hkQs{mD%;)#xSA!v8zyu}%Fn%K~#FwJS%mqdJAqVqv>_W!rdORyN;-c#@AS zK#P4ctHLtTh?vniALZ^S?R0^x%4NaHZeyezTj}Q{`JW(+z=5!wO)GzBr%As6=~-iiNJOJn}&!d5&B9PR4_ezCwW zHiSKUsLy`Y;MdOewlnQ==^`wBG`xSjYW>?Mqa)n7uu|un=#NRt=q1W%kAT|t3ByLO z5Wy+tY#)-S<}|*Y+sP2^TRRN_J`ol8=Om$0?GqcHfs#;yR8j??r`OnN3?b)!4w$(s zOT!;b@Jk>A4UOS(M!59)kq3I-is-d9?FM(lSOAZSqSaq|KG_^HC&d?tb3_=EgB%hN z%Vil=E=q)TMK9u{aI#|MwX)+7@8K>|W#2YyQ6KSdc72eU^$u}`Hr;i@VFF`5ifaDp z{U^%gDEmHKt4~VKd{y=_M!|Vr9k&;mX03ITP>%{L|C|)F{jOJ-A)j~4!8LO^pu+@~jT!^#}>_rIYfo>7lR!O%pc!^0ERMQu*AZ>+{Ro2TE8c!7_xFd(?41t!Zd;K#?HWJ>DRE>= zKQzBgsM7tmbTQY6t1l)~-684LSZO&>?j8n&hZ|fuDk0EvSpB<4lGM5^93LE~;Ul|W zNwu&!;wlN0mCe&dvuAcO|HlwoBTRMG?RhR-{X-xY`jKDIO^Jnr#0A5>pBc^}p4?4C zO^KEb#)2Z=g+0^WyuTM=XfT%}_iN;l1QA@ORtpm~Eun&YFj;A0F&R{NVGDx1ocuTG z8ob8*=Z22}ACZ^*@r$=S5+3g<@2SwB(TkpN!R_zncm+AS78b51j`%hVwej-tew_0x z5lmwU%Ps~Q(VW&I3d#p~#yw-i2`2XhxQyNLpajEtsQgfaslP}h0Yd4SR2VNjb+D$-JH~99at!67!;3surEWNPEeeJsIIDo-Xyrt8e1|Rh31=-h z6u{2Tm1pgCUlGpX`@W3f%h1dpUfU$ia30ptTZt)5!`TQ-`iOK=+4XE`sYqpCQPCOo zlxia^7TF~KeBlCl+hy+(SGuhTX950fu+;WwpAzF+4yzTO7vx&&@&1h*e&bITxpT=& z>g004;cc?RYNyb==o@4%v59i=gM}GxAh8(Yh`?K`W{K0zH@ctSsH(4deXySE%9Hvj z?$@|%(VA(!<*8Ny9J1rUb27%;a=ozCeWmZN?b14Mt}(Jxb!0y6?qy@?UJZ*YNRw~p zaVjN2&~J@7kz6dQGVw^(`ev_T+sJOV*qZuVawyVd+SDM!9d-NGq+ko_`mG^w&hMP0 zTo9n=F_TY*F6emayoVZ@nSMFnG9siZnHj(Qwt)Q=^vaYXXLqrSYJb4iPWh0oz0$71 zb%NEjI7J^RFZFlya`I&3H^<9OLam@i={kAj3eS~_jVwjR)Yq=dyF(wh_dO_7k@WR_ z&tvOXnQet{dfhDB3|B5EMW|AlynWy5!4|z|r}@<<^j=WXUQ1>*!FjTxZStBSt}1ipr^m zOvr%9U4tIsLGaxXqE3A*+da*8S!tzb>6CRP&X_1FQ(zFa6hW8=R$_9Id<19tEC?r z8XoH*ugZ6Vu1%?i*n-*k#mqE#W_}jfesbm}nfnXrj`zyHVBx?SNk{KR(DB54YFW^( zgtWix=Vaz~RK`*k<>icn z6TzT|k~cA>i}&6HMx_LDEK)oT0WELWSS~$}elJ#K?aXH`>YI$!CA0@EIq}rsCj$8E zIOY>ov3`zKw0z_Ekegs9IO#{kBpi8?QwzTQfX5)&@xb*NC zV<8tUaj-f%oNlInp-Su?9p-B=343Q&O!avK&qpLzmU^gp4aX0Z)0@=mDdP1i@ZaN z*x3@j#~$aWvKr~;x5-${F(eombH4+CJ#_nIt2kAqhDp_Ii<|yQlW^yrwofoTXMNLL zbxB3US!&LD1gpp}LRG0X>x~10m&u9_*+#q8k+JT#r7D|_t-7L^e@7OyT2?6K{%JMw zgaVmZ+*wy$iHmvS`hbbqMEfh3S@PUgu4XEAM>gC>p;mDn)^UN>x02jzddk*fv7tlJ zw!cR>f$`d1)ef%HL-q<1W`9-ztYc5m7j1~+warT4KUQk{<@3F-39Y>5s*a6qnk@y{ zfRdJQ>YaGjbynfX!2aSniks?_nitd-BVQT}JaAHB={nIp|Fqzlp;>+J6h7R)2%22H zfz-8QX72IFpYHpe6+ABw46+VVRhuhkN=hlBsynlO6{3B9b@4Fok40RSQJ`-z$dl~{ z*FFo&%@M;tyN9`#m{g-nT};;Itcsf)E|&1S`enrwR<}w2Nn|FrMlfwK)%lU9WM5Jm zyQUK45*dqVtE)SwCl}1Bdv@{d8b}9k`HqX3kbAAWRdvv(;@hxMr_gFB#X%PyJq_#j^2j?4 zFp9&a+5$ZY)EC%&Nk8Gn%({!t4VE>`*h2wX-od=cYY0XBZPC#N)JxG2B3Y9%4r*{F z+5X!^w3CfkfZt&MXmZ0?Zmv&Js$xA`;17KAemUfl23hXJCuGn^$b4sl z_H_6qJ&N~p(yw?tpah6rNheO3uB){7#%&R1QX`kUO@+er(_sd7XA@h4K{3a#RfQMP z1YyRELNtr&kcRll1N!^I&gdD@3IfjL*^ZUnm+f-MMY%ui*%G&>Z#Fqn4(5Jqk^N&1 zpC?4Z6~xPXRl6y#>SyNtS$+L$Xg;E8_>S(P(nfU@49E`<&RLwiUX4&ch`uS4rahE>PQdomV?(wKyWuC#i9QH4kSAE4D zWY(FwX05#%{XbaW$p);61VYGkx*)|nm=XR?N}&7gibp5M$PVYZAh`O;cTm+ZJvo2L z6S#stRbrX_dqgcDFg|0}>0l+Kgb>h6^QAM(T=Vx|Hty!zeY2eERiHZlW37WC(QMAL zmn*KQ;UwVdWu7hGf}aZoqiB=OVPPK^bbWDE@NPvN(e~ag_#v28ga~}3bzQXO_K)g1 z+_igjJ7?Qq_Zl@@gY&lB=I>S!viqa2xc{LFs*l?y++YiyenEWuD#6#y{at1F>*0p# zy|p?ymI@M&b94d2hTKEY_@9^NH&jV@!qVyAC<5uSGk9xW^O(9j;?^5KAu@8)sClBx zUA}5k{-_*000_K2oFWTvJ`mE(gvi(|>!`aoFiW6Kt6Aq457B8aYlSN?S4dbYXnKxX z%nPOI$S*ML&7V;qYUi`6j?OB0^+krS>uHtg=)ea-N_h3oM*H40Ia;Wt_4SRQQB?N79?H^{j4u*s*m+lU zTlZztID1X&Ha{p(CAWBg>HO|~ST=c?JWGuf8t<53jYmM}@=rw)nVy`*LEV4FI1?%z zlWOt?x?pg??JzE!@|QwGl#1j#|HRx#<3jcy=Th)JI57)+_AsVI&7T4yyPcZbzR)_-h$KrntrB92Dla8Yx6a{*{_p0-bg(RJh>=^oWa)U+n9& znUzvBc@?K+A=kjA#ga38Z*rEg!{sf?=ll!J|72--%-#XFg9z8ky78I_5Bc4+!g99` z_+$R}0s{fKFttYD&2e+<>+*T*>G}~*m!tE`cD~T{=__;Oz5KbJD|hG;J6Rx&=CAcy z{nE4p#%e^A<@<4N4x9K;!>rPg8XQw9 zoh$WN1?WwjtBleqB6W?;w3h}j_5JoUJt8bqh-I7t=M1rrY0fg)K|!6?#I+a^W*Y_F z&j;@gv3~eGXNU1Cm18;hRH!^Eo zJ+n#_M7kiz1n3~O#ae_f9nPuc$~kL^hcJDOXrkI|oi*Ef;vThU>phsYM~0+x?f!m+yWPuaUlz(R?y zmYPl_XiuVOD|08b+B@?Au+$1XS>ekQi&t z`rHDw;ckB?rrA+i6XMCbKXvFnY#F8TToHah&^=Fk)-E9QK^KXA!6w7QhQwkE6_`Qt z?ZMB4h2aU|gJ$jde@!Nb@Ww>bU%Vk~q}G^TJKg*dlC>}<#g#Q)P0ID^&!fffeE zt`=md2BXyTv% zEPVq1kK4Jd(s&}U{7XCbt80>#!gy_8Kp*bop92oNih zY_hWVsKqfUkPvEMNC=|OQAaa(8&3;XdmG2kgR%BDUu`WM%YrX1mTR%0OXR5H zUGXTd_B`x{<|pZOC&d_4Ho*qA_K%)}b{rFv3b+n`M~j=binXzo!V67u9&$^IQ?eg+ zpqQ*ZbZ#I{8vak0X2zoJi0x@XG~DVpzl6B7T8$mmE2L4Ku9gp-A?vV9rI!!Yc*wmM zztySbvi(bSb~N6sh;FIL$$*1dr>uza>79cfAt6_QDDA;4*8X4gd$`Mm?p?ke8WsY^qSA_ncQB2Q6k%`CSiWIgx!Iz3@S#6Wosa$ZK9GGn_c@ z^Hvkbs%~?Oy`}Zo&bhR_>xke+2>+ld<=Q>y8=|ed)mEFN6J%c942xoVIrTP8EFiQ{u9opnS|bBJr^ucn8Fv+aVpADec0A$p>~78@YCD&1Dw zaEndV^8Vg_6DJwn40Gx^hJ=@!WXstl%I%)mv|PfGBpPT_cmsjA-MRU0Loh({XoFnm z9Z*!XQA8Z&B`0T^m;*|zfM4gOL$4oSW$x)ONO8aUg0vQlaCLuq{eW^Sd3sy!2mhT? z;)pyrAF_8Y9k4syn(HR3D$1)$D%wl}=z5PdZJWNOc7!x@zmRLL6N{%5MVKmtxPX}6 zsst4v-ra8oUoHW#C1k^7!c{i2cCyb&kB3@+mj~R>#jaF{eSKE<#g)^G~n#mqcs66!5>Sf_5jkyU{2?q~G5~ zZox%D!@+C)dQSQ7p!|NZ1PCWzwV(@6K$Ea7kFeXGyZoHwdWfVy^pOx3)4X}R=z+={ z&P0?j8J+*o)cou{diPQE^#f`y4>6ayzXZDDCo8M7R_=}{<8Y`7n#&Fs$ukIilw2VT z>-XVfDN>)od!Qq(9vK9YU@9f1yA^wNiS&SSm;b{|<+7Wzm;QD}Eg^4XZrdf>TrJXI zL`ibTfjn?S-7!CxvW>TI(@U^$rAU0GZLSsa*UDsQ7bV?*lm0e;z!ZWzecX0-kQ~lz(sdl4SH?dA zN^2b+FU*BQH=zLakWlt?Q>4DnSj1N@XJ2*-_;EAebJdYl3Z6PcRR_8euRor5>^VJ0 zZ6~+U3hmd{+S)Mm$wRB&i6vJj-y#8tp-E&O2J1JnZiNgBQCeO zuT(*ew~e4F?&0#ekNb${=I&EfTTtxuH-=1E@~P;pp-spW=oWtna9dDoC2TQq`HsVwc7=s+i>)Rqpfnm{n^iXi=)a| zEBjHTV5Y){ELl=S7gyd$5Fmhjz&xnS9Pq|Rj5dR{a`0*21)z%#%(k~Y<41BbCK)2T zs26r4xunxX=debyLOk0>HivlYlgGn8Lq<|Qi#?AQ7{VUbqtl`}&$Xd9BZ;|9OA@p9 zDGQ1lqC7qXdw-c37JKzZ08WQ_^URop^ZD{xqoF&I?3JI8*C2vz!gomFk7_~JZ-Iuw zsNmL4Imail*8L|Y+d!U#I4F$o z9G*81)+KNG9pMVU&`Nv_{qV%>1IzR^xP^WuK{6NiHZxxI$*yo%d@Ue3herHWi_F>y z9SzRcD-sX-^Csp&A{by&F~5=TEj9Sjt7v2*u(u27!%=Q`ap*vp&#;(Nsck`G5N22o zGe%NUwfagNtX-v$1DFOD1U;uny3q#K!&{$6rdoHbB-Yf!s;0kt)6soM@R<}F2HXi< zHNWqJI`9FRw1Gt|mEF+7O?Kyx9^@O*yT_yG8Sqzv;vIm&g%`10m|J#qtD;t4ngQVS zy=>&cb?_4g7?SMWH`40H?d}))CJdM$f=?4-P}ytR$b>I)7{T>UE0PzYQ*c0!S^7Ych3*CaT%F%~Fl-pvtl)CT6zK$WR)GO#vZ?7Ru{ys)EXIq5?_V${Ge*2t4B~4I zHV~nTgopZwM@I`BTxjoqfPI#WwCaXCg9pEheY)_>D_5tI<@Yue@)mm4UGH^%rwu=V zp-Hy>K%0=A*G(2*1$9Cl`7kkMf^G%Xi8Vm*A0jXoOgmw&unVU{yxBfs<&8VjK(>K& zdlzt%iZ(!E*W-%_J_rUWiO!1z1K_Xh#8g9C&aq}lUk;2n+a!gE4ntoK9MPiChv!}I z^w7J}UJoRM;12cBy%6EvFu>4an~+z+#KJ&`yO@eCenN0Xa6ZJ9Z()T5F&@kqJa|_? zO$7QT7Km;I8Gt*W9Q z)!+?`dlR%F#z=$E>x8hkZipS6f#n_$Pc{6z8+i@tE3p3x;v+89t+^R85`Ox<pe_u^93gLkCD! za-hfFpf5VW?FbEsPBbw_wnKlM|Cc*iiCm!c8(9di{e)2gVgx#5lJ&E~? z9(qNxTN6XSVMAYmAcP@5+aIksG5ks><-o~*eStxY6!A)@P&Mpv>$HvJGd2atZs?%9 z?kD@fARlzFf1xO<7%}v%#16=n`LiP!HedGolkcbk1 zTX@t&u-t|(pigLf1Z=;aKW3qmLOh@PfLX)%>@cfK_M^ISz!UU!@!6kkM7~l86yh&GC0YN(LPtU2>Z$~K++s?hgf~EbBs)$AG7O_7isVcm&`;G8G2mZcg2Zo+}7Zl{&!MA_=&)5D;K0yp@Db3-Q5@UJJLEh;Svb z6@hq#-_!6}ltM&ZtaK4o=&CSJW8qKsFJXM~I|p6??^bX75<*k36A9?Nm$093`_CzF zrY-23$Ip%meJMAS4{O=w_U<xxqwl=w5mi^t#Av{SZ5TjTSj5ScYvo6>Hyy!>j5*taz--&)>ZlRhEH3>J3{apJ_#oT&9!AeN6*73H#A$MqDsZf{tY;18ZVV z!=u4Tn$kH(Tl8Ew9Q*O}V@mnO)kup^)Fx6GyRCyq7>t{OTHFony^J}QHu(Uw$Zt39 zzb5DjhXMZwKtR90M;?_B6usiOWNM{ofr=F-G*y?BaB~{46l0FDT+D^Y4otjG}(~xQ3(!L!*9YjytJ$xbj({`7@ zcEM@8RcO8Nw6znl6rHv!#Pr3d?NW)wlG9cmBeh`Kib$ar)`ZfACu*dJWNJcdLc11y zmYufmu`lJO?GyPjg=u?VF<)ug{!{sa%CtR&OH-Y;M;0Gco3?xKA?no}yKBjIjX*a0 z0G0im#u%r)=%5LY+cPSurF|%SVFAWq!M4ng!kv6n;lb(-q3VT~wq(|pbz#ctargF( z1qrZOefg0OG z>h_{0@u?Hymd4|Q4wMuTFoY^=yfq>&S`bNBJm42}KmbKwr@=o|aZ3P8O=yx#2oK5& z+9|rBdt;yn9go{lfS){2=W-umxmcDSH_i?dW_%R=aH6*W^>q_9OJiH5H6O0udD8x^ zxn9;%{CX9J({=yb<(pLT%a+^?y1sX@krV%4AGE!+NN4-lOZhr!acsUY{2te+Tb~4- zNqwqeMwR@Q?noQas8#nJHTg~W9b|no#jpr91&7Ac@I#DvjPPg}YKY3*VQe}j6rFX^ zP^ds<`}weadh9_NjNVJ@{sA@r;trypo%jXq#)W+om`xnI;jqQuP5IZALU&2ugLafzYslt@^_dW5f0d4RsiuhiP*$mo7H zy`)Ans^*%?!X2THv1^UP1+TcpNC{dmtQK4)O;b@ei9M6{7_Tx?PIj$C3aFSa5T%}~ z%gh?j(&N)Ki8aerK3E=wTUec>>=dBJIg{%6}` zuy?!dE;y(Q4z>IshpRg`g%1qB!QYqJ@@Q!{7VW5{HL!>*G8|(n96!ZozbTp`G*gc7 z;mx)lz?xC^shXJ_5HZu;C&y;9l`J*7NoFg;47#Pv+~~s5#JCO*Gr0(L8vF6;pnF5z z&m5AIG@G~db*l=tgNqWp{T{zy;@vaskOhr%G^z0jiwoqmK=ywcp2b^qr~h- zj)|#ZZDk7B0sV*jXk%|_$o_6q@iECy7dGA#TrSo9?U{P9MRYmZ2W{9*d}X3oe2ljJ zBfp&lx;Q__O7wY#%#!8O2Nvxi(fK1En?H7Tp=>ML+jftFcBQzLQ)PuB7E#ZZHfjHo zd{Cck_j5RIBFw6^JkYPWdj%L@6Qjl@}*zPBM>&nXB#M%$0Xh?|JCAEH*3%eH*^O@Q|?Bn|QIv zI=YoBddWtjP;J4xyqXUWj2>+ip1OZiLb2lG7sK*VnKqwUzcO5`v=ue%AlrAdrng&^dY1g~ z%E|j`0{1A_zKQEUT(0xIO62lhk83nEejr>r#?4j9$Y;F%7C**Q>%JELq6lL*;oD_} zsK3!ENIioxFVgrM!zTKQb-{WI-!A@y9{#72`Ix_4U&&*B#KzuHj7KO7r?0kZL`OR1 zWM8)gp+Y(r`RS(exKJ^1aHWm@u5&%Qt%@mc(79-pjfj+xMl) z36=QD&GJ@yLFnYG?=n4iH3f?3l``fn{_^A7lPoIur0-11Jg_;Jeyr&HE>zg@_eT|r zR*Bn-UqXi!)rJk6(^&Jo|GTXAI|+qJ6uZ8LiP*G8$p7YKQ|1@S#tmWH2ZQYr#17tD z*w}wBF#dp&XI5F@H&=;d^^F2+TMETB+e}zxO}&$7y>4VwV<=9_y7R-n@13lw2Pzb{ z^y8)ZTLb?(s}l0%*g27p@5Dc8iMf>Tun59rQH;=jk%M#rCDn13dXrhO;WQ1yHrjxRPIH4^c%w?L$wF9yFv zV&nPtu_s%W`z_Ph`RU$dCNWiuE_zAYdGe6g>0Gb#Bgj`fc4*&PUE8F=*oN=m^T!WN zF7y&L4y(eCFTf^@9@M*|_H|o9;T^3___5up6N}$Aq!;a5bWTBBsYX+AdqY#Q$TH+Y z`!XL3^;XZPEq6s0@|&|=bVklLU6Q|?@Ix9uMnJvvXRmT^9MsV~Mf)i##X z!&knOEW5o`r|H(0_ocWbl6xnkj%Ho#Kedc7YTe;eM^eP27q)CLHM<#!{meHMK)CaTO=F0tkVURZLAhj>Ovo)Xj>U4TcNB^4ZQD5ANDELd{# zX8ge%2J7tk$@B5$-FW1(zb`YsQy35OT=HJg2ruv2H3=;8V+U>NvP&XvBd$iX_ZM?>TA<~?!mlbrbeP8QWV@mxA^Y7?>L#*5;M${XT- zB1IGxy(~901}3S#N)(CvC0{;zx=+b|9U*!Duefi5Vs;ViDh2g7jY(E~M8(I}NfbVc zN_Sw6Nzp+u>mn4YOvq%=Xk2E$l6;~498PUqvN+Y2!1 zJNN#hK+SWHv67jKt>9?jc&LCigS9EH6zQ^S7hsH|p{`gyIhSDUt*l?BA&_XA6={$N#oXP>*>Zh7Vy_dNuqbXi1zIe$XMHF#l&T zYmrjp-YS#t=^e61zh0DAxoG!Z<#bAXo@;pilyq0ps?bNfS@vPKCpY2=-PbLm3lzSd zJ9FrKtbF=Eu=)SU>AaiSnI7~q{PgmekH)%NKDIOY!w3g$+nMRNH3)p0nd#^vFC3cl z*>185%lTxY6bRcwnCTd#+)t6qrw6kfW3+`DiM1X}KTF;QErTetE4GYtcH2u!gi`_H5U8*CH7{X)g#qc*dLgl)7xUcsN`Z^XyjRa ze0+p4Ko=U8GNw)$vTO-e{dUqjn2*}NSS}zH9?*&xuflKp>4;h&7+7rm%gM>26~ioulPq|MJ%u|)DF0S} zKv&`b!BVM)oM?cz(R=lt`b=i%WL43EE$;?W&VK2{nf`S{-fnYF+#PFDv@Pn|=jCf7P30oi@sHsZzT_#jub!kPpH#NJ`z>7frq?bt`2?jK zn+~AqOYImhRxrhjm$@dyqSj_?bvlC+PCcq(DK^%jlunm)>+8XtGC0gx8JLKsKEUh6@bR+v$UWBgc`vFx`^JZnb zr9=9SoOSp3?WE0!ZbeTKl&@{dikt%nH z4{vmro~b~gRfqe9@8E7-zHaoZhUH!$mHt5^$vcU5pnucP5tZEDuBXRdZ$J+(khy(L zrIzp7OSD?pZNBTl0Ex^Q0Amy{(`tk4TFbcG48hZY^;!<9(V3%6T zqDFkm^?#fZJwFlQaCee@?XIKIL<<#J-_~;0{MC|k+iENDuR4(DA0l)L<%G}-B`Fc~ zVYGF?U;Og@x~s+8EJ_Ad%YU*n4lUj^#vglizpK$0##Hi(Va^q0$+*9k?Xyje^M9}* zM}5j=bI*svV&Ue?6N9cAo#7Ai+9t(*_CWBLfUSFuaOxW65|4)VOJxZOGCi!o5ifv$&y2aOgP>bxrxvCNh z(Gr0N#br~&wmtH_{MoSVQn-rix>~&Iy05n`XVo37+DA!Rp(porceR#b1;6uYea}%> zjLB}dTZ{Ym??l;&^Lg2uh`ZDz_lw$|PhKaw5wU1jQmw-ihh;z21a~)KjAgB=6BuJ3 z`jOwYYV+^?9DiM5@MtQ$6uUw_=K7@)e*QXtl_3E&2Yb1pqHQk~nIts_(VXB;flw`- zPNhzwR76vvl&RsUKqa|6@sMW~{!XM(SZ;pw<7cJ<*K?_b$;7&l){ct$io*-)hSrFf zvE%C=Cr9|0NgjGSaxo|#XS~_m;bKcD$q6MI>yVn9EQR9NOEE0&4rC<-y$;fSqgYE} zjG`Kfmb<@IHO0vCP262aOzLgHEs-`p*FqsJF*E#nrTMRRw8R$)OMSy~sZ6ozzQ4Nf zSe9NnOlKI?$4A@5M{av9hC1AGVi{d{eb1=Su-Wrr3>HVwHSXTxPb&YYubxsHa4I=F z_8je}F6FLaSEmrkCN$!MOI-T>qn6!?|2QuhdiICI{o`4L0)8T-O_ z@lx-iXtvlTN2}`#^o~XMOZP5VZ15tL5_2|J=3uf}#)Y`W6l_+V>0_LSNPy4{>x5UoJqCbaYa&c;Pb1T zg{(RcYHp{|b3GT8Btrd3+>i~cn}YdWe`#$?q?u#4^pF>Q{P zeUMFOp1*~yjokUDnEJR>@R+fkyEyt{d7S^s-5(Qb1T_n$E-q`0F=M_oy09#AoG7_v z&m>XHa|7~#NvuVMxIKE0t+d+OB!d@L(UI<}`g7!cga!=+N5u{=+p6;1-u#C^F~LER z;zL?`ZM9-TnyxUl5@KK^k4o%6bmLe+`GyZ-W!^QGSL`zb%Etc^6O(;^L;8_{#v~e< zYDCHuICtiBKPxl#(bswc3ubw(U5=Lp_jU#X)N7lw}KPRZ^T{kCD|}p{rR)r^J-u5 z`CrR2n}yg2nsu;Ny6&f~Kczd`%cuvCf~XejE2X$2d1b~cEz7sqY2DnV6;|Km*}rE6 zGP#-XBSQP-Tk_K)mj11i7mhSb41F?H=_Ym&u~IJZPcvi-wZ!WRD8bE?J~cum5VPd`yf z)RK1*>DaalWY=o+g>zam&1vYN_;ZAnVjby7DBQ{!M`vZeYJS%X}#D z;U%M^#|QJT#@~$DwkrRc=eC@7iJoN=KaRu_liQfu6z});3Tu7eRY>8J);#JtBij|L zXme)Sz(4iRritHHm&vf7TBMWZ`8icP+O_@Ago2sO@iPT7eWyR`wwm5h8?Y#nTrzOI zQ7FM_T%hRt?r$B6e0m-CF9|o`HTjg^6dbM+K?rw$?T-y`!vAw{WD0d?F{bza zfTX|Cq9alXqn(_x!JTfDI+dI0dRfV3bs_pue(L1O<S^w+Dg1+h@J^+0%d+<3dOX2V@3R?uiI1og?UC#M|ZgWtnMOZl5T~|b6nSb z0lggP8S8eqtl+qST5tgegM?S^qlPdfK8$QL~^spAEY@~=-i?(N!R>h>&ygIo~ z*GE|MhBD4H5tD0nQpc_4xS3`2$v z>3OBj7wtFNMKY1B8`1|FE!IC?RVZyzQSQILe89%%VoFmXA&Pna#7?UIZ6&iTrQ;_; zmc296wmO(4S8AW$b31EH+eW{Hxj}%sC2RR+b;0&n(=2U5xw~qinCxox6U65_seaK_ ztqQt-wR*+%A!*~`7-Jvg2bSvCx|2i0A-ZacfvIWmM6mT2v?XpPA^)_`2R$eG@tHfh3dU z6q=EszKO_ac7{ZLoVzwff04e)R?8Is4Vl-E%V(<_BG-Ntmg9S7(c1rAz&v(QnYQ5; z>vo(%q;cM`@=4L-_p0E2_6B^G<2^+^!RfiC#RrpT^8U1{DJnidwx?Lk} z8G@R$v0lqW`;5t^lQJt3sHH7lvCg9SCl`Opr>Bey5W}9xD11l#3@)B3l^(D$CWbR-O}S(38RG<95_v^@-7wYBseHrrT-hP=Dyie9s%2)_DYKCb_2i7+Bx$_S z)FI@^YEwpdM8>)tN9C;_LPeH&G-+Kljvn$!@7U@t{=C*SRQ%(2mUd}zx~9`eZ?zMudH%N-huqQs%g<%{!J^?dnnsR#x7s7 zGUdBxvBKHG%+k(=k)ePQzj)yBm9Z~Hqujl(rmbA+)OjcnZB9zw zw9mY3(5dc{%7jeuFkcti#z|53{hD)vmeH_YIFw=0T8-r6OZ4TOBT?G&=RCCP@p9UD=f zNPNDc)W*g#A@0tDLILcmImiWzDVpmMmns=-13MVG{ZElaNSQxIK|I<&3 zeGIo7r?UBjR56wz@YSG2}sMLfKeH!&@yavx?lP}PeQ7M`PfATbX&hfP z5{P?I$&R4B=(DEe1Pg4bYI~KRtwb#;aHH=opeo>;m=oRi2Op3GaN$aVQ>^BTb zQ#&2wnNpH`SS_z|dt8hvX@BMG=5wreBwq%@?7E5Jf|1*22Gx&tWZRoA=}W(p9CdyF z)S)v}Eu`q}edof9ZuTa25CtDBavsV0L>D#7LGoMceJ1;E4rc6jbVmjboKQ%7Qe~4w zw=p${2_x27x5thtr$p*iuB<5NLJiVZf40_hNM{F`xrz-_;wq7=8H(?181xpZQq1Lu z?G=4yYx(8UR0Q%q4CU5%N};S?NglPnB0&}JzedX3R_?t^fN&Kd@wQGY?ae!WGR>5b z`<*i@SeTesVr8n1E`6eXD7mK6G2=jA$X)pt=RLY@)9;dNFW<5~yYIc3a|+t?%)To6 z@{2if%}EbaEjIV2)~rKR)i3=PB>U89z5nO7zLKNW-%5XVVpC!&wl^r%i-axtyDHM~ zjaS84Z{LhnqKoLALSZzWy2h2|Vjg8H@*9$fN!z;Ae{5YJDlSJ6cj-E2wI*M`yE>&y z+XN?LKb+)ff|JBAdY5GUw7|GVOHA{?NfQa7Fkkz$BllO@e@2;2YILzIj%Z3Hg_yKo zj#2VElC~|0Nef5E+}qQIkvBXwvhsAr2M5i#YkGSt%|qD>7NlV!A4j2xpYB!-l8h9e zIxH!du3UL~EOx3yJ>-abEyF^%Q@Lh%V_eY3#@3_Rz84Dp>6+-j(O4w=OEbXcv3*x8Laa;05e0xrxGwb$m)#3kwy+HZ9?&{KH4E{`{f#`QjaaG4xJ$JIGl) z+ww5c)qaQ5>ZHzVYt#?pV>1#dqS(lq)!z@3zeQo*mq{E5b3-b{Z^*loYL!SNqThBN zv+Cb}Ns;+nE(T|nXqAsv6tfqLC4T#+@+`rjm#8C_al~oZ{-}Sw*3NY{dmKarJZocZ zVZs695p?Ap@)&xN~0-&K;mBu6H;9Tcm& zb>`6bgxG=hM^V?P8W(*GzTQf#)pym|>VMRsS9RGBmU*Avwqn&q9q+?paRRx!(rkC9IX*GV)j2Bt zl=EPTTy!V#i_>DUv+jO-pOI0a!ndsNAHQB87=nxMPTl-uwMor-4V&(!k=Qt&^b1;Y z7vz<9XmBm>QJP=O!C`E)~SLES=0_U8ouj|ah$wB_YU`829 zf&NZUZNXvhPe2vZFhc+SCkr-e*5F_-MTx~ojSa)%AOmt~Z4~KLJWCJcP0>#%Zgs}s zkjo*o*!F|8HeRrhDPp2yf{|s5-o*T4h2((zV@3e_fTA7Gq9063cyVKZ5CyXagoOWX2mK>0PM92)?Kn@rqI)E%lZzTt# zY3)d99X^+101rgtgn!+B&gyqSNxWs_y`<#=4@Dy~C{9~O?+OlgMbC|V9sjE2Boxco z_%l#&bdDJC<=)7=4h5lbTsZ$RG|(F|thoRONHN$>e;PV%=jUx`4;^lPD-VM=g`NSS z_;x|QGy6IZB%u50h$Ouc)<~O5A12)x@KE|~ z(A0s#wHpO54j6o^2J1P?b~wjS(W3CVSyK)8-39UWw^f@h)#@}6mhewv@9woTDn^Z* z3c(ZbEcT^Z4@U9n7ROIm3phX$*+m#BKpFxAKf?!k1@aw!QM*y5D;7E+QI_sruVl$Q zu@c>;>%$*Fnyz>i|K22R>SsA2JyA=;QI1X9$GF9kw9$-sIND!p^ zZ6w5m8!I2!u)$*89rhr)gcy}z(80{_a|t`|*nwg%yBJnl7Cod*j3C*JO)tj>B-$+# zOdvG~dvmAfFb+5nj&tA`&}PQ+gwsf7d~U_DuuwpLVS2iGTgc^rN)o4f6*(rkfc?SQ zSFNc5_<}CM?|3-s0;$9z}wB>WW(crQ51wOjLhz=F@bc@=rJPrm6Mfg3j4 z)96cyj*xgZG0A{Tazp^oxxSk>+u?la&P)NwNg>8V@|Ax?+aWYxlF~?n!prx?mW>c# zSl%{dRSd`_#J*72mX8$XNrUe3+}^_30QfFKA@MrknHzM;QixJ>NEsMRJby@sb8$$G zHP6{db0kx;?n6Zca0$-9h7Nnlv*OkiEeP~_t4$t~T|*8{9^g*d8X!HlBFue&ksHxd z-%k?IU>g{4`VS)Z01O5^BoAo3`tWnNi;2-fSVJZNXL=xbq<0z<@nQ7C3s>Uz!GTw| zNH?R+R>vTT$k7!8f*jd`L|*_2*wG1BVD02~0g&Rx!XqD~(@ZYGH`nx6NU+cwF5O8f z)gZ;jZ^;ZyD=11!>MmWG7fx3)mK8YIvn+N2JfQlwtJC84j#H(hB8=kxbPXf`N5AcI z|9$hNRGgzDHy~dUh(sh9vvl5;9tx1!H3&fPj2FZzNDJ^zN4~l5JWKV_Ncc7{y;bxd1z2@EFgsbSp|T6jcyx<{4|)Nica4I0^nsAy zyeTL|O@;PaKu?WiHuKNA)}kC zTt4J8^Hgwz2ILTPZ??u03&v(}A&=1@;7mw~bY>V2HRBQapx{OsszZ)E#ieYyB@M^< zONsTl3yw@Y4G~7wC8&|<1*D|1SOD8WP+Q{82OOQ`4qzc$%}HArPe$+{U0VA|1zPM) z`aC--+ZEI93@~Z;m9FSd8R0(2phIr}sqPXKrS7X?!}L|YQP z9E%tP?VX@(LZl1}%d-4N6|-?@Xbc|x@ui@_6e5w1uoQxn;QZ0IaYN0me$rg)HXae zS)S0ZD5&eq#aa140*-8(aP)7KUI^!MSZU#Kd{}ARl9O!r&y8~&Wb{b*vOZR@a98Lc zlo}{oSFR(+enGf>Jp-3LTmyv0IUp@belp<&o~?gbo(2>)Nr6Q0VOZ`0O-u~8L%`4) z@eN-6Jqb4Gpf}6Y;G7>uw%U;G_{PHMK|GL$-qGk{IvC<_=oIUn7PTzw8qD0@J2EvtuIm6p?@eq!DRwJ95fQbcR=78#k`=IvIF?{MIY>Z6R0&(UcL2;w z1r^M0`VUO!R9z zUGTV?_gH9*`BvZ~5M2KPjcV+L!>xC3@1_nl>gPc2NALu9KZHoA2K0o$DRp*9=k^O2 zuX|mc$R!a}i)h6u!~DIsA!I9)sn~<29eb*+=&H`^#YXE1s}naabRtj!yXsZF8cyE& z0Z?z1EI`E2st!o;S?R*-rnSm!k^^!=Ucszvw?$j>Trdn6#P;8YVgx>SDkT5%5TuZ@n&AJ#cfg2acB)euAutv+=AP|T_Ffu96956RIs%sU2 z>MzI!PW9DYP;4ELyW%}#Yal(mYF;%bcvA5_OKCuBAcb5G$eC(g$cAf8Cb_v4rJ%|- z<;kVu7-?Q;UC5%|lF#XCLTVbOyHY|=CtiZNQ%+78pWAg&H8snN23BABf$?PQ$hvA) zNGl&zz{o9nbWuww%mf?|f(E1^(P}^*2$x0lw`wc+0&-tc4A2CJ9}f>mEn-_+Tj}x? zI5!PIM*nFe){qT{5E7w0NovSRHXL-1-e<}EY@34+IbXdvU%jwtsBe-Z32Cv4y{ZKk z;2*yO>jgya!DTm#4#?hVVlsXViyV;ZWT2(CX*)zbkOQJ*Mp>3$&le@36eQBpvOF&b zWHv1uY1`8RS%A&!T>!P(`7YpcWFUgduEsCv;$UKt7kTCuM+SVpJzac6G?H?K%HsSO~#$ z!Y5>C?VQO2sT7JOWrPsy7FuM?%^oEO3Q?=WWt!?PSOuiTK<)cA~B!lDnk36C8s#AFo! zI2hrU731x`W-NBx5gU1#Vy-fGcDcP_~74}oN4c$LEGi#Hn3G}7 zxok8{(-1d51xmp^L&MSO&V0nC*$mI6&PKQDFyf803c2yE;ptfKz<3r&!)C^-#qVlIg$yzw|6bTF*&sM+*U2^w(nRn?>=-rqMxKnw9X(I| zzvlu=n~5-5{Ji>aSuRw|)YK(srxI$p{T4cd$3W}O5DYf?OKAR*VrT?Nh4x%vYB?E@ z!@cG0BYw}9V(Cc%_31i}OfK+hQVn7c$Hs&}KSUjiIu5W^s~QkR-NZl04JL_TZdilm zKXpnE9(hShZk{dnyshR71e@!09b+Blz5*011cLKYI{^P9d%`imgwl27>!ZJ=8!ReR zY;8+i(fsnjuR_*HjYJyWSti*UsB`}Q?(g}I^@6;|d%k+^HFI8|p@H}MKW~Jo4ewlm zkz08g3k4GpsfP|cTWi9&oM9+*Wdv4k!j*9j0i#I5K&iIqjcH-%zigr%d}-+6fm;L| z5Si`+3#uBaY~q1EaEJ@7?i`H)!j%We z+y&GPZCM(;bbv+AIFAkcTQf92jYyEmbtCxFHh2_~VpL8dV$;B*EFL)i6h#MWCqo3a z`Pzi4=!YW}i!h4!=8JT;xC?G(10!VLKEpaHVlkGv)3HNRNRWZ7Aq-VFn^!>Wv1~W@ z@rG z6*KDSF<4C5VZ@|2BMpJ@kdpOr{)0z0&4+n${!nOF!B!TM%_Og~SD%=iWW$(i%eG-! zd3hjDVRB+}Vhs;hv*|7Q4QN&wTUA&6Y+$jrVIS+5W{|Qxd?X(o+gM-y^B#)l$lKUi zT+{C%`IIo=AOBQaAeDejkRt?M0Hj+Pbgm~tZ-G3JE2maakPc-Jmtj9=Rx=Rt=`BAu zb6#ZA^u8eJA3JcSRSMZma{p9?@Ks4RpWZ<~bNctRmN3H3jaVz;$feit4aY5bLxxnk zm24h!`g2vdi4tf>M|6Hk$yDEus8jbrR)<%2A0XUG5X5#6mWU>ofj?M~Yq<-=E{QSi z`rQaNj4@yVKMC_3buFkXz9l?*Z0EUvcZ)}jv#6TyR;A!pc61l2iDDa+diV6iPdgq; z!Fw&KFJ-hM2u0sJIqh|;U_l$AgHSP*0E9u`^OuiUS0exbUvfQ^QgzAeTd`v{U%Oc1rTn zvd|`j^5-15F~UeX*uu@ zm&0%IrQi>e!P>(_DZzCAiAp*eu#X>(o8AN zveGli%(ujd1XuNbrk5 z_850zu%wmgN<)L3%{P;9P;Sa9xSNS}aYo9b=?ed&bYFM-ySny9Z83DGt_DrvL9c{pj?I00Nj|7p zjUs4o>EULOhsabfVUg@qS2v{JaF*M`^%>PwL@GcSGGUX009l9OhQV=kwh*VwNqua2 zw)2S@Siy&Y$9!e!DR|n~`{A(&3V!50!TB+M4V zL=R6BW|9jzc;dz7mkHXEDJVa04cD+rPiSOCFIT@P6LP=!Xg!GaVKOf!@Q-Lbh8_2Qk zfDM}#I8d8v2wzDH+mH9Q(`s)`_?#+!+@a5WWO(;-0R_%Uw!JmCweg!FHfJ`!n&`BxCRhawkYG+CdLB1-UOMN~dvX)#PiBYxpY;i}A3^1ewb+ojsw95NM-RR0wR)H9c^C5O~Jz+1eh>g7Ow>0_0eM_IeM3V6x>e{@l%N zuQId-2DcN-0lT(s`<>lhejyXfI00VEmAxI&DHn+_VP3l1R$}KF0=5xIy-0Ng*H$y`7?OM$-^km)H64*{b;Hn6* z7h8l4N+4ZQc^En&LtyhveLuHt4UJFc@F_4HG7+!r*5Dv_0NhD-r z+n9F}0zh-Czr^-pvuJfXHv%j$*3Fta!2#OJ58-zji_H*8Rl&p&&@ABE39U)=Y3Xw> zx+9DANf%5e6O_ZXriM7~F%WoeAaQwuaDwGp_-_J*h3$f^K2n&A%X(;zXA7G`e@uUq zJ1}Pafcix=3r>ZuwPA;TVZasEwqV^Z)j{Pv@>3ydn(fmr19(nWdSByEKv)`f!(X>v z%~qQHIzdRts^&V}_7Xwyqu)*Is+HOcJnf5e6!v+tO$EU%Tg=6nfJ#X@6$j6DIu0G- z!RZIVfq_T^vkFis)-LNlR%Ww7cy_6GEIV8FN8y6g?UmYgUawD*MvjVnU^t1%KTluR z8PVT_BVYYUXgv!lNJK1l>-h!4#5V;Vw zY~eTe>j#hp5y?ysuikTsC)`&*!_6l-+3v^>_Ur16Pf7RieXQi!$;DHAR~`T}di~ZggdCZtgZIbVvmP zbUOodE(CNTj_oLJ1#}e8bO%V31x`**&ISw<8X;Eh-RdQrGFs)DxP)aSVafcm9$ zI3B_*!6#~8t>gx-0R%$1+5!mjZC90Q!K|RdL;|_?pfFwAeyN12$kM-LgbTkOB)udt}Sp zL|c_fIp^(zG|M)b;@T6!VRGbUP_eAKtWR#)Qka=W;n|oyx$KY&(no1>tQ9cBl-@)+ zNM~!HRUPE>Ki=JS0qoh2*3hB}K`%tc#@b)$NmuH0m+EvfGR$BP`QpENqi4gd~hXKS27z!Hm2GBei^# zM?%#bdE2Vp2J&HGDTLWfJ*EpX_SUpaFG8A-;kxdLG$cjQAA=2c+e{WL{p3NyOjeU_ z@??M5v7kB@$TG$}s?N~_UnBRiBRM-a&j(*{pqP($N_TesqQX$X~u4cjdf2x z(jDHjPP&H~H%d>Lo3h6cPSZitOsqSk83F?0Gm%UU?M?FPuuMfkMAs6=%yl0}4NO83 zdOY0ZWdlNmH+NFi8+aWVKRi1CbO>((046$*-0ssz0uNZnJr2et7d8{KLS)6L5^y6& zSg^+pI7r}=v1!Ib|D`|&3QO=w@um(#~*li(+gB2VkQeQ%No@T6GS^Wb3Lv!R|Pg(P=SUp}1M`%c-1X!xj zwP%JCSH?$H{{x!k(=46r?~AuAcOWARd{qFR<~9O3a^&@;A+YeJAkaZTilw8NA^2SO zj(=qd^L;tYBE`NeXE_{!Cr$E=8Lgv;r3n1Vlr%%e1T3Xn9C`9gEJS**)2x*|kdr>8 zC#4$%hOkja1}9QSJ}^0e2I8dTq$Dh|B~uO=07gK$zj>LkT+$uNt|KH)PA-Lq)9U7d zWdmHfCG%vklXOQ_bj_(+$cy{u{b^!xIA5?g&a^y+OdIQ&^ z*`qm-!xm6A0*oExqXJq)&8<-;5S;0PN0CFUoNpv>dZWWq0v$dk56Na->0J7ht3v}{ zAGrrx6{Nnr(k84^Tp|nrLC1eNMUKc3F@-xM&Gf3$4gm3$PT(X-zrLb|}@^j!e{F6i|f-h7g0LI>BXlPJX^?WCg>?0zHyC51ylO z7Fxn`Om(vgTf3l1UUmp|Mss|tI_d-(@_y9!L5NP(7V9B(bz$@r^Gmhkdv`rwC6I$q zD+mT^D|JM*!lP6dKEdG(|IsjnUYj>F@tm2WL77qY2Z-26L}iRE$(OP=<_c6yLy9{z$*M6O&~)ibfw_#1hwcLbPhr0 zD*ylX_xRJ|V4+AM*A4H3=K}%w!*^R20Qm|)Kq3pN>RURD-~#K#uMEJFO0TBx1WmDt z+Rz8*7hmz<9oMi)IqJ$Zo2SdhroahXHhoM~Q5w3hl8rvSg!9*3u?gmYc8Pp{CoX2fzXlD`s1CW{Gj$sO{hK5J`i>)j*U)G0EjaT$|!Sl$l*jrHfAC zOVQK{=Pz?j>Mb>m6Mnx` zNf@Y6n*8aut*h`!Q}DD@ae@{VXw%koV$s=O_J=x}D>l_lDcxmD3k1wisjw<$mu5rS zwnieHVZeiE2DgNgOB^bw#;R-`EvA0`LmW_LX))^qrO_YEu$S6U4WV5YI;z^+OPJDFsU)v0b=eUnlU# zKs22|16x(wzz~ZW5>TQ--&gceKGdXq-<9Buv!RZIIq2V=rYZ=*jpl?8`)lZrvO(MJ zoX1>i-~(mzFy+Qg6w>jw}+;8ZBKWO243&~oK@Z9mA+R?D=OU3CKtfSj-bDnuyq zz)PsjVFT!v+zsfX=)GC6izD_t+v|)DbVM*@49Ur58PM1a0tk^K!dHXN0kQ5zHOzj9 zmqRD5cpPT7KETHWn~sj>5j*)7x!C&nL4)V_4QmI;2KgEb@Ws4f;nWaem-)Uo;qul919w&44Moi~JH3T+k zaGi}=XtWj;D%+)ZLd}+@=FWFYGrWM)dvsn?emxpMh4&^} zvS?F4d9Bn&fHTpfMAk5?JJeV{9wpi7a^McNq|dW4B?`D`#0c}Y6RJL*RMW%L%sz`> z(4DpdLBGUAB)B7A&@9Fg1teo}b3q(KJiWsbJ#o^-jZebI=lQs7@lc_0q3O5{_sFi% zS;$1%Wt5w+SaWgWSY=;mYa*8Cz)87!hiUSk!L*Ou>Stwfr5`J+);o$bm67==LN4-Yr z2y!mo@)15NbZFk51dBp;F6r{Xcyns-g!EEm;Wa9-I^{kr8-Zgy%T!<5PHK_(^GQt* z78X$hd86h?3Hj}8*v{Z`>y;Z0hH|oGaJxk?OGA`@E9kgVBK_J8kRRTs1 zL$i2>h|vZ-yfes-!}9S92|3h)$m>u)+6BO%XvHg)1-_R9S)NAm_xI-jwNV#3JY6pu z^Nkt`L%fENn)PV%iHtAfn8qlSIfV+0817 zD&&sdqm0UzNg?jZ7GSiI&ja~ZMwhSDkW;dw`Ij9k+9}KO@&cmVNjdVDk`qG?Afn_* z6F*xAR3l<%9-L^0mT2euFj^@Swd9y_5gZ(v71fcNaZjsjYt7F$rhJTqm*-{S(@(H zrUydc@Qwfml0q!qxxye6$JsCSwjyk#Bl zy;;N=#8Cu} z(y~~b-5Rsx@A#AhVl~o4H?mDQi{AoDSunH3kO!h3UtlSiok8HW)D=(yiy2xJ!lJpe zh*EH&c%O&%COL#MtOkVkI6-MP80=ozshv7YOw-vDQ)UApf*t}&YA!nx26(J-_a$t$ z6G7G$=Q>OWppb*bc)q2jSCu?@pDReEhMy#HB8l6h>&WhT5~5C|?=JPCP@MOPa7u;Awf8z?PUE7BOz9H4bE8yC`rHcJq9l94Cg)F zDD-tiQyg{y4G@AY4@Zi24k_Qm0sSzI8yKYMbOiv;SpS|BT9o-FQ;8M95;@*VjiXqW z5Csd{wmmG@pZ%BZ0>wB@;C58B5Ddw)U?Cmdwzma~1`!Sdbr`r1n!qz|Ws~S3r%*o& zwn_-kC;#xn?PEP8>!UP(`bjDColETgmID>>JeL8`q;MFy0(7vRSUv`x2JGevunI^H z2O256dU-Mq9Hw%YpFxSqcqE|#xeuY@d3(gGqkbKVA^$|c4i=t8uZug0E;RR&?q49n zZ9MnU=%Aq21|h?ZT@Y%AO4HM5mA+O?)Fk9P?1aqpi4CZp|9VvLL0_+S$ z{;L#q0LQ2ZDXJt}g&JCZp9uD5kP53zwb2`R|DLiR+%J}ZnJ zqvGTRIpQg#YE{8NerNv(o@_t!DbFYStW+&5I|Nq&EMh8ekZuE{DalaFbRy}}EN2<= z%F}em9tLg?;z4;E3?8HwiZKT%ShQsV1rTY*37iBl;K*$W!GH~e=ipWV22={i>V7A> z_wL685(0DyW)KbnjCN3V?Q{di01GVi(+%++JCCV!u<1-m78eF|bW@|c0lxw4x5-Jwhp)e6vg!$48rx6UeKgtC(g;eI{$)7LFaE z!mItr_PMO)Qo^hcO#%#*;B&&P1~1MEFhR1-JTb`DvryDkx%E@G9L*M&7#zCAr#CFi z)(1Qv$kX24o|oo#xi9(Am30tN_Ol+yt~tjz`PSz3k;Ku{)$FiQ6R|8h3)&#g`vVqkZQME=&LnV(Z$PL`n$FE)jqTtd+aDt~&Zi z0y0@5n`P{AF~;U>Fq&pr-ltsc2ovJU`fM^*fP<@_<*Uv}!v-8obk3>*hU z)c^=WssJ_3;mP@g^VX>i8UhT{J^3}2fX?<*ISf7FPq#Ep^D%Da&Hc?(b6sm)1Cj(a z8F9lkV}pgTw;l|0WMG(MA#5OB(ryC;h6gcqF3PHa)fmW68p1?D)x;=@LIE4lK#2BH z)#w2Pyk;yE(2o5>n)fm0G zShNNkvUmn9r`_8gxlK|ooDVLn7tq;K6#xg2Smc|vkEtL8KnDn0d)-_^1dJpO=PRI_ zp4ws*j4;GVWU*ZIT*y?P7jYR|N%h>IDgtIS+3gTZY;6oX7cTKD;!;o8(P!FCYMK!r zQd4}~tW9&$Al00<+kNZ@Js z0;o)z06Qw+bdE52h|uqR!59cA^R+XC-%Rl7KsQHcC`~!#Wm0@zY7F-stQTGI5XD!I zU5nfe@)l9u^R2GfG>9@h6XEMcivZI`W(3pITh(vihGZf0|hM%>pic1?KYql-gW;j39}(B);d!4O+kf->RhtxUN?@Sr<<^A-J_VJLwry z0p`pIZcSz5w>8BH&v@i++B?J44el{`8;?SpDY6uN!vMW41HrWvnbOc5zyvKdBK_nC zixIO}7be4S@+H!Y-JXH8VjTGa2|)sLb9XZ-19WZ$1awdZbTTV8mu?1hTJJq|xC8xW zb5R`ocsFig%W_)+Iv2|b>J>W~S(-^o5J>Lj6diJ`F)v&&+jPjW&N9>)FMJ65$h++k zycYp4OpQbcY-Cq+!|U1UFc>e~*=+YY#v0QI;ef~>-J3b*a$PWqg@UeKY=d(dTvjj* zQGS}p?tNn|=z$J>Cmn_u>tf&UWe;ACawqC18+IEX2oMwV2m)R(5ygJqV*ax|O)y+I z65p8VKGTalf)lI=iGX9!;SYy}9|2@Gq8zTu)naYqqaQQuDzap(Kow#%3E)V)z`JH@ znEM_+1QS?t?97=Iqh5ART@7B^estO0 zgfB+!Xdo39`b{Cw(IXN;Df0dQ=4!S$O)AhIuS80@ zpb5=fs6#oAu?y$N9^3-k&?i3NkR6a{PnIy0l+)-DQUxM9d&nog!wkFcDZ)lLJg_tg zQxS_5z>nnISruG%?FU-dAua2GizSGWf2Qy==Z#uXw8=q<)tsVal=RqSM|SjDM!nD8 z5!WmO0mrB#8P`avlhGPm`2zf}k*Cz%tM0dGMA1wG4ZkNr=7PHsfntG_vF0{cgnC;+ ztJ-iJYDK0Z%-XyCBO2<^90AZt0TUtkR}tTCL~zh_5Q`Oj5kdSKjt>Hac9T7I2^Umj ztbJKz`B08T#9f67fI)!B_t5AvS0bAgCA4-msI5ab+FmCtjMye%{glWYbGyV_atXjs zcid>-H3!{@t%agVk~gA6{AR>sdbq_0%Q#|6gxAKd;Y*lbJQ!T~jT!%dof4316fWCD} z8!pCH3Sj-lBFG&Thk{5&acLf*c5^M2<4AXQjXZWRv=F95w)+$<#P(}+I!pn`Pf({& znvD@%g^M$j28ZajC4Wd1i5vmA$ATLW%y=F5y!R`&ZahJ{Eh%krC z)8WzUfgsWqks!OVVh7PqDfg2K^Hi!(1WqVcxLNUvgH?zVju3A!P%`JAm+IBH3?1JoI-CoLUg`-*C%a6i|k6>N{n7u=AE0@nqKh(@g z4cE%puAt4}w8apuP+wovI{%ApNZT8MkKej0fV(wxfemK*71e@};q|ZNh?q3im?=!V zg{>POsMGE6)l7)$-RF3b-K*#|chR>eoHMqO2KLyC56bHip7GF|D7)eVJb(8OQyYp* z(drGt$#8HuwMa!C@N6+6Jx29Z0}(R|ga{h}{`d5-!E};DzU+3wHz@j1mzwk-$H624 z$NW@9XxsGipn>E6uZssGLeu1v2Bd5ojoSgqhExxxsnjGvg+p-C;Lw%y=FZ;9;U|;t z+`bZ-f=&fmu0&{WQO^dx;JNljXrbEyI=LQ8rZ=O|LO6IM?}12D(U@!ZLAs<4{-D94n~G*@HB6LDO@k?O4sMNT?VAUCP~W(4 zaLBqMjnEXnctBG?fQ0(lpa(l$9a}1})%Q$?fFfK$Q>U1kM8S&PNZg|T(AprLz00oOg-MU_ zsRedai)CXfOSDXG!>m}UW!c8h8VX2>NuSvuSGEU!qeHHdTPz4m?wK_~wp=LD=S-yu z;|1R|H`~M8`&=CvCfvhVQ%ev@yRJpejdKbXRC5DT2n!sfa|c+1yz_KD< u^d;aV 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 Cm3yhs~C={i(wzM>ixY1wKfoBmyPg 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

reZI`ud@^L;qx>lu3z8E&W!U3abY_l@eaia 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