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 902ef3842..0d591eff2 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
@@ -1980,7 +1980,13 @@ public final class GLRenderer implements Renderer {
@SuppressWarnings("fallthrough")
private void setupTextureParams(int unit, Texture tex) {
Image image = tex.getImage();
- int target = convertTextureType(tex.getType(), image != null ? image.getMultiSamples() : 1, -1);
+ int samples = image != null ? image.getMultiSamples() : 1;
+ int target = convertTextureType(tex.getType(), samples, -1);
+
+ if (samples > 1) {
+ bindTextureOnly(target, image, unit);
+ return;
+ }
boolean haveMips = true;
if (image != null) {
@@ -2183,45 +2189,46 @@ public final class GLRenderer implements Renderer {
int target = convertTextureType(type, img.getMultiSamples(), -1);
bindTextureAndUnit(target, img, unit);
- if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired()) {
- // Image does not have mipmaps, but they are required.
- // Generate from base level.
+ int imageSamples = img.getMultiSamples();
- if (!caps.contains(Caps.FrameBuffer) && gl2 != null) {
- gl2.glTexParameteri(target, GL2.GL_GENERATE_MIPMAP, GL.GL_TRUE);
- img.setMipmapsGenerated(true);
+ if (imageSamples <= 1) {
+ if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired()) {
+ // Image does not have mipmaps, but they are required.
+ // Generate from base level.
+
+ if (!caps.contains(Caps.FrameBuffer) && gl2 != null) {
+ gl2.glTexParameteri(target, GL2.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.
+ gl.glTexParameteri(target, GL.GL_TEXTURE_MAX_LEVEL, img.getMipMapSizes().length - 1);
} 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 (caps.contains(Caps.OpenGL20)) {
- gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, img.getMipMapSizes().length - 1);
+ // Image does not have mipmaps and they are not required.
+ // Specify that that the texture has no mipmaps.
+ gl.glTexParameteri(target, GL.GL_TEXTURE_MAX_LEVEL, 0);
}
} else {
- // Image does not have mipmaps and they are not required.
- // Specify that that the texture has no mipmaps.
- if (caps.contains(Caps.OpenGL20)) {
- gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, 0);
+ // Check if graphics card doesn't support multisample textures
+ if (!caps.contains(Caps.TextureMultisample)) {
+ throw new RendererException("Multisample textures are not supported by the video hardware");
+ }
+
+ if (img.isGeneratedMipmapsRequired() || img.hasMipmaps()) {
+ throw new RendererException("Multisample textures do not support mipmaps");
}
- }
- int imageSamples = img.getMultiSamples();
- if (imageSamples > 1) {
if (img.getFormat().isDepthFormat()) {
img.setMultiSamples(Math.min(limits.get(Limits.DepthTextureSamples), imageSamples));
} else {
img.setMultiSamples(Math.min(limits.get(Limits.ColorTextureSamples), imageSamples));
}
- }
- // Check if graphics card doesn't support multisample textures
- if (!caps.contains(Caps.TextureMultisample)) {
- if (img.getMultiSamples() > 1) {
- throw new RendererException("Multisample textures are not supported by the video hardware");
- }
+ scaleToPot = false;
}
// Check if graphics card doesn't support depth textures
diff --git a/jme3-core/src/main/java/com/jme3/scene/BatchNode.java b/jme3-core/src/main/java/com/jme3/scene/BatchNode.java
index 1b0d5e051..d2d44dd56 100644
--- a/jme3-core/src/main/java/com/jme3/scene/BatchNode.java
+++ b/jme3-core/src/main/java/com/jme3/scene/BatchNode.java
@@ -62,9 +62,9 @@ import com.jme3.util.clone.JmeCloneable;
* Sub geoms can be removed but it may be slower than the normal spatial removing
* Sub geoms can be added after the batch() method has been called but won't be batched and will just be rendered as normal geometries.
* To integrate them in the batch you have to call the batch() method again on the batchNode.
- *
- * TODO normal or tangents or both looks a bit weird
+ *
* TODO more automagic (batch when needed in the updateLogicalState)
+ *
* @author Nehon
*/
public class BatchNode extends GeometryGroupNode {
@@ -108,7 +108,7 @@ public class BatchNode extends GeometryGroupNode {
public void onMaterialChange(Geometry geom) {
throw new UnsupportedOperationException(
"Cannot set the material of a batched geometry, "
- + "change the material of the parent BatchNode.");
+ + "change the material of the parent BatchNode.");
}
@Override
@@ -122,7 +122,7 @@ public class BatchNode extends GeometryGroupNode {
setNeedsFullRebatch(true);
}
- protected Matrix4f getTransformMatrix(Geometry g){
+ protected Matrix4f getTransformMatrix(Geometry g) {
return g.cachedWorldMat;
}
@@ -133,35 +133,44 @@ public class BatchNode extends GeometryGroupNode {
Mesh origMesh = bg.getMesh();
VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position);
- FloatBuffer posBuf = (FloatBuffer) pvb.getData();
VertexBuffer nvb = mesh.getBuffer(VertexBuffer.Type.Normal);
- FloatBuffer normBuf = (FloatBuffer) nvb.getData();
+ VertexBuffer tvb = mesh.getBuffer(VertexBuffer.Type.Tangent);
VertexBuffer opvb = origMesh.getBuffer(VertexBuffer.Type.Position);
- FloatBuffer oposBuf = (FloatBuffer) opvb.getData();
VertexBuffer onvb = origMesh.getBuffer(VertexBuffer.Type.Normal);
- FloatBuffer onormBuf = (FloatBuffer) onvb.getData();
+ VertexBuffer otvb = origMesh.getBuffer(VertexBuffer.Type.Tangent);
+
+ FloatBuffer posBuf = getFloatBuffer(pvb);
+ FloatBuffer normBuf = getFloatBuffer(nvb);
+ FloatBuffer tanBuf = getFloatBuffer(tvb);
+
+ FloatBuffer oposBuf = getFloatBuffer(opvb);
+ FloatBuffer onormBuf = getFloatBuffer(onvb);
+ FloatBuffer otanBuf = getFloatBuffer(otvb);
+
Matrix4f transformMat = getTransformMatrix(bg);
+ doTransforms(oposBuf, onormBuf, otanBuf, posBuf, normBuf, tanBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), transformMat);
- if (mesh.getBuffer(VertexBuffer.Type.Tangent) != null) {
+ pvb.updateData(posBuf);
- VertexBuffer tvb = mesh.getBuffer(VertexBuffer.Type.Tangent);
- FloatBuffer tanBuf = (FloatBuffer) tvb.getData();
- VertexBuffer otvb = origMesh.getBuffer(VertexBuffer.Type.Tangent);
- FloatBuffer otanBuf = (FloatBuffer) otvb.getData();
- doTransformsTangents(oposBuf, onormBuf, otanBuf, posBuf, normBuf, tanBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), transformMat);
+ if (nvb != null) {
+ nvb.updateData(normBuf);
+ }
+ if (tvb != null) {
tvb.updateData(tanBuf);
- } else {
- doTransforms(oposBuf, onormBuf, posBuf, normBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), transformMat);
}
- pvb.updateData(posBuf);
- nvb.updateData(normBuf);
-
batch.geometry.updateModelBound();
}
}
+ private FloatBuffer getFloatBuffer(VertexBuffer vb) {
+ if (vb == null) {
+ return null;
+ }
+ return (FloatBuffer) vb.getData();
+ }
+
/**
* Batch this batchNode
* every geometry of the sub scene graph of this node will be batched into a single mesh that will be rendered in one call
@@ -234,7 +243,7 @@ public class BatchNode extends GeometryGroupNode {
logger.log(Level.FINE, "Batched {0} geometries in {1} batches.", new Object[]{nbGeoms, batches.size()});
//init the temp arrays if something has been batched only.
- if(matMap.size()>0){
+ if (matMap.size() > 0) {
//TODO these arrays should be allocated by chunk instead to avoid recreating them each time the batch is changed.
//init temp float arrays
tmpFloat = new float[maxVertCount * 3];
@@ -257,6 +266,7 @@ public class BatchNode extends GeometryGroupNode {
/**
* recursively visit the subgraph and unbatch geometries
+ *
* @param s
*/
private void unbatchSubGraph(Spatial s) {
@@ -343,11 +353,10 @@ public class BatchNode extends GeometryGroupNode {
/**
* Returns the material that is used for the first batch of this BatchNode
- *
+ *
* use getMaterial(Material material,int batchIndex) to get a material from a specific batch
*
* @return the material that is used for the first batch of this BatchNode
- *
* @see #setMaterial(com.jme3.material.Material)
*/
public Material getMaterial() {
@@ -428,14 +437,6 @@ public class BatchNode extends GeometryGroupNode {
+ " primitive types: " + mode + " != " + listMode);
}
mode = listMode;
- //Not needed anymore as lineWidth is now in RenderState and will be taken into account when merging according to the material
-// if (mode == Mesh.Mode.Lines) {
-// if (lineWidth != 1f && listLineWidth != lineWidth) {
-// throw new UnsupportedOperationException("When using Mesh Line mode, cannot combine meshes with different line width "
-// + lineWidth + " != " + listLineWidth);
-// }
-// lineWidth = listLineWidth;
-// }
compsForBuf[VertexBuffer.Type.Index.ordinal()] = components;
}
@@ -528,53 +529,7 @@ public class BatchNode extends GeometryGroupNode {
}
}
- private void doTransforms(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, FloatBuffer bufPos, FloatBuffer bufNorm, int start, int end, Matrix4f transform) {
- TempVars vars = TempVars.get();
- Vector3f pos = vars.vect1;
- Vector3f norm = vars.vect2;
-
- int length = (end - start) * 3;
-
- // offset is given in element units
- // convert to be in component units
- int offset = start * 3;
- bindBufPos.rewind();
- bindBufNorm.rewind();
- //bufPos.position(offset);
- //bufNorm.position(offset);
- bindBufPos.get(tmpFloat, 0, length);
- bindBufNorm.get(tmpFloatN, 0, length);
- int index = 0;
- while (index < length) {
- pos.x = tmpFloat[index];
- norm.x = tmpFloatN[index++];
- pos.y = tmpFloat[index];
- norm.y = tmpFloatN[index++];
- pos.z = tmpFloat[index];
- norm.z = tmpFloatN[index];
-
- transform.mult(pos, pos);
- transform.multNormal(norm, norm);
-
- index -= 2;
- tmpFloat[index] = pos.x;
- tmpFloatN[index++] = norm.x;
- tmpFloat[index] = pos.y;
- tmpFloatN[index++] = norm.y;
- tmpFloat[index] = pos.z;
- tmpFloatN[index++] = norm.z;
-
- }
- vars.release();
- bufPos.position(offset);
- //using bulk put as it's faster
- bufPos.put(tmpFloat, 0, length);
- bufNorm.position(offset);
- //using bulk put as it's faster
- bufNorm.put(tmpFloatN, 0, length);
- }
-
- private void doTransformsTangents(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, FloatBuffer bindBufTangents,FloatBuffer bufPos, FloatBuffer bufNorm, FloatBuffer bufTangents, int start, int end, Matrix4f transform) {
+ private void doTransforms(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, FloatBuffer bindBufTangents, FloatBuffer bufPos, FloatBuffer bufNorm, FloatBuffer bufTangents, int start, int end, Matrix4f transform) {
TempVars vars = TempVars.get();
Vector3f pos = vars.vect1;
Vector3f norm = vars.vect2;
@@ -588,60 +543,76 @@ public class BatchNode extends GeometryGroupNode {
int offset = start * 3;
int tanOffset = start * 4;
-
bindBufPos.rewind();
- bindBufNorm.rewind();
- bindBufTangents.rewind();
bindBufPos.get(tmpFloat, 0, length);
- bindBufNorm.get(tmpFloatN, 0, length);
- bindBufTangents.get(tmpFloatT, 0, tanLength);
+
+ if (bindBufNorm != null) {
+ bindBufNorm.rewind();
+ bindBufNorm.get(tmpFloatN, 0, length);
+ }
+
+ if (bindBufTangents != null) {
+ bindBufTangents.rewind();
+ bindBufTangents.get(tmpFloatT, 0, tanLength);
+ }
int index = 0;
int tanIndex = 0;
- while (index < length) {
- pos.x = tmpFloat[index];
- norm.x = tmpFloatN[index++];
- pos.y = tmpFloat[index];
- norm.y = tmpFloatN[index++];
- pos.z = tmpFloat[index];
- norm.z = tmpFloatN[index];
+ int index1, index2, tanIndex1, tanIndex2;
- tan.x = tmpFloatT[tanIndex++];
- tan.y = tmpFloatT[tanIndex++];
- tan.z = tmpFloatT[tanIndex++];
+ while (index < length) {
+ index1 = index + 1;
+ index2 = index + 2;
+ pos.x = tmpFloat[index];
+ pos.y = tmpFloat[index1];
+ pos.z = tmpFloat[index2];
transform.mult(pos, pos);
- transform.multNormal(norm, norm);
- transform.multNormal(tan, tan);
-
- index -= 2;
- tanIndex -= 3;
-
tmpFloat[index] = pos.x;
- tmpFloatN[index++] = norm.x;
- tmpFloat[index] = pos.y;
- tmpFloatN[index++] = norm.y;
- tmpFloat[index] = pos.z;
- tmpFloatN[index++] = norm.z;
-
- tmpFloatT[tanIndex++] = tan.x;
- tmpFloatT[tanIndex++] = tan.y;
- tmpFloatT[tanIndex++] = tan.z;
+ tmpFloat[index1] = pos.y;
+ tmpFloat[index2] = pos.z;
+
+ if (bindBufNorm != null) {
+ norm.x = tmpFloatN[index];
+ norm.y = tmpFloatN[index1];
+ norm.z = tmpFloatN[index2];
+ transform.multNormal(norm, norm);
+ tmpFloatN[index] = norm.x;
+ tmpFloatN[index1] = norm.y;
+ tmpFloatN[index2] = norm.z;
+ }
- //Skipping 4th element of tangent buffer (handedness)
- tanIndex++;
+ index += 3;
+
+ if (bindBufTangents != null) {
+ tanIndex1 = tanIndex + 1;
+ tanIndex2 = tanIndex + 2;
+ tan.x = tmpFloatT[tanIndex];
+ tan.y = tmpFloatT[tanIndex1];
+ tan.z = tmpFloatT[tanIndex2];
+ transform.multNormal(tan, tan);
+ tmpFloatT[tanIndex] = tan.x;
+ tmpFloatT[tanIndex1] = tan.y;
+ tmpFloatT[tanIndex2] = tan.z;
+ tanIndex += 4;
+ }
}
vars.release();
- bufPos.position(offset);
+
//using bulk put as it's faster
+ bufPos.position(offset);
bufPos.put(tmpFloat, 0, length);
- bufNorm.position(offset);
- //using bulk put as it's faster
- bufNorm.put(tmpFloatN, 0, length);
- bufTangents.position(tanOffset);
- //using bulk put as it's faster
- bufTangents.put(tmpFloatT, 0, tanLength);
+
+ if (bindBufNorm != null) {
+ bufNorm.position(offset);
+ bufNorm.put(tmpFloatN, 0, length);
+ }
+
+ if (bindBufTangents != null) {
+ bufTangents.position(tanOffset);
+ bufTangents.put(tmpFloatT, 0, tanLength);
+ }
}
private void doCopyBuffer(FloatBuffer inBuf, int offset, FloatBuffer outBuf, int componentSize) {
@@ -653,11 +624,11 @@ public class BatchNode extends GeometryGroupNode {
offset *= componentSize;
for (int i = 0; i < inBuf.limit() / componentSize; i++) {
- pos.x = inBuf.get(i * componentSize + 0);
+ pos.x = inBuf.get(i * componentSize);
pos.y = inBuf.get(i * componentSize + 1);
pos.z = inBuf.get(i * componentSize + 2);
- outBuf.put(offset + i * componentSize + 0, pos.x);
+ outBuf.put(offset + i * componentSize, pos.x);
outBuf.put(offset + i * componentSize + 1, pos.y);
outBuf.put(offset + i * componentSize + 2, pos.z);
}
@@ -667,6 +638,7 @@ public class BatchNode extends GeometryGroupNode {
protected class Batch implements JmeCloneable {
/**
* update the batchesByGeom map for this batch with the given List of geometries
+ *
* @param list
*/
void updateGeomList(List list) {
@@ -676,6 +648,7 @@ public class BatchNode extends GeometryGroupNode {
}
}
}
+
Geometry geometry;
public final Geometry getGeometry() {
@@ -685,14 +658,14 @@ public class BatchNode extends GeometryGroupNode {
@Override
public Batch jmeClone() {
try {
- return (Batch)super.clone();
+ return (Batch) super.clone();
} catch (CloneNotSupportedException ex) {
throw new AssertionError();
}
}
@Override
- public void cloneFields( Cloner cloner, Object original ) {
+ public void cloneFields(Cloner cloner, Object original) {
this.geometry = cloner.clone(geometry);
}
@@ -704,11 +677,11 @@ public class BatchNode extends GeometryGroupNode {
@Override
public Node clone(boolean cloneMaterials) {
- BatchNode clone = (BatchNode)super.clone(cloneMaterials);
- if ( batches.size() > 0) {
- for ( Batch b : batches ) {
- for ( int i =0; i < clone.children.size(); i++ ) {
- if ( clone.children.get(i).getName().equals(b.geometry.getName())) {
+ BatchNode clone = (BatchNode) super.clone(cloneMaterials);
+ if (batches.size() > 0) {
+ for (Batch b : batches) {
+ for (int i = 0; i < clone.children.size(); i++) {
+ if (clone.children.get(i).getName().equals(b.geometry.getName())) {
clone.children.remove(i);
break;
}
@@ -723,10 +696,10 @@ public class BatchNode extends GeometryGroupNode {
}
/**
- * Called internally by com.jme3.util.clone.Cloner. Do not call directly.
+ * Called internally by com.jme3.util.clone.Cloner. Do not call directly.
*/
@Override
- public void cloneFields( Cloner cloner, Object original ) {
+ public void cloneFields(Cloner cloner, Object original) {
super.cloneFields(cloner, original);
this.batches = cloner.clone(batches);
@@ -736,7 +709,7 @@ public class BatchNode extends GeometryGroupNode {
HashMap newBatchesByGeom = new HashMap();
- for( Map.Entry e : batchesByGeom.entrySet() ) {
+ for (Map.Entry e : batchesByGeom.entrySet()) {
newBatchesByGeom.put(cloner.clone(e.getKey()), cloner.clone(e.getValue()));
}
this.batchesByGeom = newBatchesByGeom;
@@ -745,7 +718,7 @@ public class BatchNode extends GeometryGroupNode {
@Override
public int collideWith(Collidable other, CollisionResults results) {
int total = 0;
- for (Spatial child : children.getArray()){
+ for (Spatial child : children.getArray()) {
if (!isBatch(child)) {
total += child.collideWith(other, results);
}
diff --git a/jme3-core/src/main/java/com/jme3/scene/Mesh.java b/jme3-core/src/main/java/com/jme3/scene/Mesh.java
index 87dff6aec..7921624f7 100644
--- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java
+++ b/jme3-core/src/main/java/com/jme3/scene/Mesh.java
@@ -1013,6 +1013,18 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
BoundingVolume worldBound,
CollisionResults results){
+ switch (mode) {
+ case Points:
+ case Lines:
+ case LineStrip:
+ case LineLoop:
+ /*
+ * Collisions can be detected only with triangles,
+ * and there are no triangles in this mesh.
+ */
+ return 0;
+ }
+
if (getVertexCount() == 0) {
return 0;
}
diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/Sky.j3md b/jme3-core/src/main/resources/Common/MatDefs/Misc/Sky.j3md
index 0a3b627a3..f7887a5a1 100644
--- a/jme3-core/src/main/resources/Common/MatDefs/Misc/Sky.j3md
+++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Sky.j3md
@@ -12,7 +12,7 @@ MaterialDef Sky Plane {
WorldParameters {
ViewMatrix
ProjectionMatrix
- WorldMatrix
+ WorldMatrixInverse
}
Defines {
diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/Sky.vert b/jme3-core/src/main/resources/Common/MatDefs/Misc/Sky.vert
index 4b5c67eb2..0877fc03e 100644
--- a/jme3-core/src/main/resources/Common/MatDefs/Misc/Sky.vert
+++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Sky.vert
@@ -1,7 +1,7 @@
#import "Common/ShaderLib/GLSLCompat.glsllib"
uniform mat4 g_ViewMatrix;
uniform mat4 g_ProjectionMatrix;
-uniform mat4 g_WorldMatrix;
+uniform mat4 g_WorldMatrixInverse;
uniform vec3 m_NormalScale;
@@ -22,5 +22,5 @@ void main(){
gl_Position = g_ProjectionMatrix * pos;
vec4 normal = vec4(inNormal * m_NormalScale, 0.0);
- direction = (g_WorldMatrix * normal).xyz;
+ direction = (g_WorldMatrixInverse * normal).xyz;
}
diff --git a/jme3-core/src/test/java/com/jme3/scene/PhantomTrianglesTest.java b/jme3-core/src/test/java/com/jme3/scene/PhantomTrianglesTest.java
new file mode 100644
index 000000000..91f190e9d
--- /dev/null
+++ b/jme3-core/src/test/java/com/jme3/scene/PhantomTrianglesTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2017 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.scene;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.DesktopAssetManager;
+import com.jme3.asset.plugins.ClasspathLocator;
+import com.jme3.collision.CollisionResult;
+import com.jme3.collision.CollisionResults;
+import com.jme3.material.Material;
+import com.jme3.material.plugins.J3MLoader;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.shape.Quad;
+import org.junit.Test;
+
+/**
+ * Verify that collideWith() doesn't reports collisions with phantom triangles.
+ * This was issue #710 at GitHub.
+ *
+ * @author Stephen Gold
+ */
+public class PhantomTrianglesTest {
+
+ AssetManager assetManager;
+
+ /**
+ * ray in the -Z direction, starting from (0.1, 0.2, 10)
+ */
+ final private Ray ray = new Ray(/* origin */new Vector3f(0.1f, 0.2f, 10f),
+ /* direction */ new Vector3f(0f, 0f, -1f));
+ Node rootNode;
+
+ /**
+ * Cast a ray at the geometries and report all collisions.
+ */
+ void castRay() {
+ CollisionResults results = new CollisionResults();
+ rootNode.collideWith(ray, results);
+ int numResults = results.size();
+ for (int resultI = 0; resultI < numResults; resultI++) {
+ CollisionResult result = results.getCollision(resultI);
+ Geometry geometry = result.getGeometry();
+ String name = geometry.getName();
+ if (name.equals("white lines")) {
+ assert false; // phantom triangle
+ }
+ }
+ }
+
+ /**
+ * Attach a red square in the XY plane with its lower left corner at (0, 0,
+ * 0). It is composed of 2 triangles.
+ */
+ void createRedSquare() {
+ Mesh quadMesh = new Quad(1f, 1f);
+ Geometry redSquare = new Geometry("red square", quadMesh);
+ Material red = assetManager.loadMaterial("Common/Materials/RedColor.j3m");
+ redSquare.setMaterial(red);
+ rootNode.attachChild(redSquare);
+ }
+
+ /**
+ * Attach a pair of parallel white lines in the z=1 plane.
+ */
+ void createWhiteLines() {
+ Mesh lineMesh = new Mesh();
+ lineMesh.setMode(Mesh.Mode.Lines);
+ float[] corners = new float[]{
+ -1f, -1f, 0f,
+ -1f, 1f, 0f,
+ 1f, 1f, 0f,
+ 1f, -1f, 0f
+ };
+ lineMesh.setBuffer(VertexBuffer.Type.Position, 3, corners);
+ short[] indices = new short[]{0, 1, 2, 3};
+ lineMesh.setBuffer(VertexBuffer.Type.Index, 2, indices);
+ lineMesh.updateBound();
+ Geometry whiteLines = new Geometry("white lines", lineMesh);
+ Material white = assetManager.loadMaterial("Common/Materials/WhiteColor.j3m");
+ whiteLines.setMaterial(white);
+ whiteLines.move(0f, 0f, 1f);
+ rootNode.attachChild(whiteLines);
+ }
+
+ @Test
+ public void testPhantomTriangles() {
+ assetManager = new DesktopAssetManager();
+ assetManager.registerLocator(null, ClasspathLocator.class);
+ assetManager.registerLoader(J3MLoader.class, "j3m", "j3md");
+ rootNode = new Node();
+
+ createRedSquare();
+ createWhiteLines();
+ rootNode.updateLogicalState(0.01f);
+ rootNode.updateGeometricState();
+ castRay();
+ }
+}
diff --git a/jme3-examples/src/main/java/jme3test/texture/TestSkyRotation.java b/jme3-examples/src/main/java/jme3test/texture/TestSkyRotation.java
new file mode 100644
index 000000000..f57e71fd7
--- /dev/null
+++ b/jme3-examples/src/main/java/jme3test/texture/TestSkyRotation.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2017 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 jme3test.texture;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Box;
+import com.jme3.util.SkyFactory;
+
+/**
+ * Simple application to test sky rotation with a cube-mapped sky.
+ *
+ * Press "T" to rotate the sky and floor to the camera's left. Press "Y" to
+ * rotate the sky and floor to the camera's right. Both should appear to move by
+ * the same amount in the same direction.
+ *
+ * See issue #651 for further information.
+ *
+ * @author Stephen Gold
+ */
+public class TestSkyRotation extends SimpleApplication implements ActionListener {
+
+ /**
+ * objects visible in the scene
+ */
+ private Spatial floor, sky;
+ /**
+ * Y-axis rotation angle in radians
+ */
+ private float angle = 0f;
+
+ public static void main(String[] arguments) {
+ TestSkyRotation application = new TestSkyRotation();
+ application.start();
+ }
+
+ @Override
+ public void simpleInitApp() {
+ /*
+ * Configure the camera.
+ */
+ flyCam.setEnabled(false);
+ Vector3f location = new Vector3f(-7f, 4f, 8f);
+ cam.setLocation(location);
+ Quaternion orientation;
+ orientation = new Quaternion(0.0037f, 0.944684f, -0.01067f, 0.327789f);
+ assert FastMath.approximateEquals(orientation.norm(), 1f);
+ cam.setRotation(orientation);
+ /*
+ * Attach a cube-mapped sky to the scene graph.
+ */
+ sky = SkyFactory.createSky(assetManager,
+ "Scenes/Beach/FullskiesSunset0068.dds",
+ SkyFactory.EnvMapType.CubeMap);
+ rootNode.attachChild(sky);
+ /*
+ * Attach a "floor" geometry to the scene graph.
+ */
+ Mesh floorMesh = new Box(10f, 0.1f, 10f);
+ floor = new Geometry("floor", floorMesh);
+ Material floorMaterial = new Material(assetManager,
+ "Common/MatDefs/Misc/Unshaded.j3md");
+ floorMaterial.setTexture("ColorMap",
+ assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+ floor.setMaterial(floorMaterial);
+ rootNode.attachChild(floor);
+ /*
+ * Configure mappings and listeners for keyboard input.
+ */
+ inputManager.addMapping("left", new KeyTrigger(KeyInput.KEY_T));
+ inputManager.addMapping("right", new KeyTrigger(KeyInput.KEY_Y));
+ inputManager.addListener(this, "left");
+ inputManager.addListener(this, "right");
+ }
+
+ /**
+ * Handle an input action from the user.
+ *
+ * @param name the name of the action
+ * @param ongoing true→depress key, false→release key
+ * @param ignored
+ */
+ @Override
+ public void onAction(String name, boolean ongoing, float ignored) {
+ if (!ongoing) {
+ return;
+ }
+ /*
+ * Update the Y-axis rotation angle based on which key was pressed.
+ */
+ if (name.equals("left")) {
+ angle += 0.1f; // radians
+ System.out.print("rotate floor and sky leftward ...");
+ } else if (name.equals("right")) {
+ angle -= 0.1f; // radians
+ System.out.printf("rotate floor and sky spatials rightward ...");
+ } else {
+ return;
+ }
+ /*
+ * Update the local rotations of both objects based on the angle.
+ */
+ System.out.printf(" to %.1f radians left of start%n", angle);
+ Quaternion rotation = new Quaternion();
+ rotation.fromAngleNormalAxis(angle, Vector3f.UNIT_Y);
+ floor.setLocalRotation(rotation);
+ sky.setLocalRotation(rotation);
+ }
+}
diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java
index 5581e1f17..ab7a3bc74 100644
--- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java
+++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java
@@ -4,6 +4,7 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.jme3.asset.AssetLoadException;
+import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
@@ -56,13 +57,13 @@ public class CustomContentManager {
}
}
- public T readExtensionAndExtras(String name, JsonElement el, T input) throws AssetLoadException {
+ public T readExtensionAndExtras(String name, JsonElement el, T input) throws AssetLoadException, IOException {
T output = readExtension(name, el, input);
output = readExtras(name, el, output);
return output;
}
- private T readExtension(String name, JsonElement el, T input) throws AssetLoadException {
+ private T readExtension(String name, JsonElement el, T input) throws AssetLoadException, IOException {
JsonElement extensions = el.getAsJsonObject().getAsJsonObject("extensions");
if (extensions == null) {
return input;
diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ExtensionLoader.java
index d0dc85125..3f32be4bd 100644
--- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ExtensionLoader.java
+++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ExtensionLoader.java
@@ -2,6 +2,8 @@ package com.jme3.scene.plugins.gltf;
import com.google.gson.JsonElement;
+import java.io.IOException;
+
/**
* Base Interface for extension loading implementation.
*
@@ -19,6 +21,6 @@ public interface ExtensionLoader {
* @param input an object containing already loaded data from the element, this is most probably a JME object
* @return An object of the same type as input, containing the data from the input object and the eventual additional data read from the extension
*/
- Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input);
+ Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input) throws IOException;
}
diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GlbLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GlbLoader.java
new file mode 100644
index 000000000..7ca499c9d
--- /dev/null
+++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GlbLoader.java
@@ -0,0 +1,58 @@
+package com.jme3.scene.plugins.gltf;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.util.LittleEndien;
+
+import java.io.*;
+import java.util.ArrayList;
+
+/**
+ * Created by Nehon on 12/09/2017.
+ */
+public class GlbLoader extends GltfLoader {
+
+ private static final int GLTF_MAGIC = 0x46546C67;
+ private static final int JSON_TYPE = 0x4E4F534A;
+ private static final int BIN_TYPE = 0x004E4942;
+ private ArrayList data = new ArrayList<>();
+
+ @Override
+ public Object load(AssetInfo assetInfo) throws IOException {
+ LittleEndien stream = new LittleEndien(new DataInputStream(assetInfo.openStream()));
+ int magic = stream.readInt();
+ int version = stream.readInt();
+ int length = stream.readInt();
+ System.err.println(magic == GLTF_MAGIC ? "gltf" : "no no no");
+ System.err.println(version);
+ System.err.println(length);
+
+ byte[] json = null;
+
+ //length is the total size, we have to remove the header size (3 integers = 12 bytes).
+ length -= 12;
+
+ while (length > 0) {
+ int chunkLength = stream.readInt();
+ int chunkType = stream.readInt();
+ if (chunkType == JSON_TYPE) {
+ json = new byte[chunkLength];
+ stream.read(json);
+ System.err.println(new String(json));
+ } else {
+ byte[] bin = new byte[chunkLength];
+ stream.read(bin);
+ data.add(bin);
+ }
+ //8 is the byte size of the 2 ints chunkLength and chunkType.
+ length -= chunkLength + 8;
+ }
+
+ return loadFromStream(assetInfo, new ByteArrayInputStream(json));
+ }
+
+ @Override
+ protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
+ return data.get(bufferIndex);
+ }
+
+}
diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java
index 6536bdf2f..a97d40017 100644
--- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java
+++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java
@@ -75,6 +75,11 @@ public class GltfLoader implements AssetLoader {
@Override
public Object load(AssetInfo assetInfo) throws IOException {
+ return loadFromStream(assetInfo, assetInfo.openStream());
+ }
+
+
+ protected Object loadFromStream(AssetInfo assetInfo, InputStream stream) throws IOException {
try {
dataCache.clear();
info = assetInfo;
@@ -87,7 +92,7 @@ public class GltfLoader implements AssetLoader {
defaultMat.setFloat("Roughness", 1f);
}
- docRoot = new JsonParser().parse(new JsonReader(new InputStreamReader(assetInfo.openStream()))).getAsJsonObject();
+ docRoot = new JsonParser().parse(new JsonReader(new InputStreamReader(stream))).getAsJsonObject();
JsonObject asset = docRoot.getAsJsonObject().get("asset").getAsJsonObject();
String generator = getAsString(asset, "generator");
@@ -455,7 +460,7 @@ public class GltfLoader implements AssetLoader {
return data;
}
- public void readBuffer(Integer bufferViewIndex, int byteOffset, int count, Object store, int numComponents, VertexBuffer.Format format) throws IOException {
+ public Object readBuffer(Integer bufferViewIndex, int byteOffset, int count, Object store, int numComponents, VertexBuffer.Format format) throws IOException {
JsonObject bufferView = bufferViews.get(bufferViewIndex).getAsJsonObject();
Integer bufferIndex = getAsInteger(bufferView, "buffer");
@@ -473,8 +478,17 @@ public class GltfLoader implements AssetLoader {
data = customContentManager.readExtensionAndExtras("bufferView", bufferView, data);
+ if (store == null) {
+ store = new byte[byteLength];
+ }
+
+ if (count == -1) {
+ count = byteLength;
+ }
+
populateBuffer(store, data, count, byteOffset + bvByteOffset, byteStride, numComponents, format);
+ return store;
}
public byte[] readData(int bufferIndex) throws IOException {
@@ -489,6 +503,17 @@ public class GltfLoader implements AssetLoader {
if (data != null) {
return data;
}
+ data = getBytes(bufferIndex, uri, bufferLength);
+
+ data = customContentManager.readExtensionAndExtras("buffer", buffer, data);
+
+ addToCache("buffers", bufferIndex, data, buffers.size());
+ return data;
+
+ }
+
+ protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
+ byte[] data;
if (uri != null) {
if (uri.startsWith("data:")) {
//base 64 embed data
@@ -505,19 +530,13 @@ public class GltfLoader implements AssetLoader {
input.read(data);
}
} else {
- //no URI we are in a binary file so the data is in the 2nd chunk
- //TODO handle binary GLTF (GLB)
- throw new AssetLoadException("Binary gltf is not supported yet");
+ //no URI this should not happen in a gltf file, only in glb files.
+ throw new AssetLoadException("Buffer " + bufferIndex + " has no uri");
}
-
- data = customContentManager.readExtensionAndExtras("buffer", buffer, data);
-
- addToCache("buffers", bufferIndex, data, buffers.size());
return data;
-
}
- public Material readMaterial(int materialIndex) {
+ public Material readMaterial(int materialIndex) throws IOException {
assertNotNull(materials, "There is no material defined yet a mesh references one");
JsonObject matData = materials.get(materialIndex).getAsJsonObject();
@@ -571,7 +590,7 @@ public class GltfLoader implements AssetLoader {
return adapter.getMaterial();
}
- public void readCameras() {
+ public void readCameras() throws IOException {
if (cameras == null) {
return;
}
@@ -616,12 +635,12 @@ public class GltfLoader implements AssetLoader {
}
}
- public Texture2D readTexture(JsonObject texture) {
+ public Texture2D readTexture(JsonObject texture) throws IOException {
return readTexture(texture, false);
}
- public Texture2D readTexture(JsonObject texture, boolean flip) {
+ public Texture2D readTexture(JsonObject texture, boolean flip) throws IOException {
if (texture == null) {
return null;
}
@@ -646,18 +665,24 @@ public class GltfLoader implements AssetLoader {
return texture2d;
}
- public Texture2D readImage(int sourceIndex, boolean flip) {
+ public Texture2D readImage(int sourceIndex, boolean flip) throws IOException {
if (images == null) {
throw new AssetLoadException("No image defined");
}
JsonObject image = images.get(sourceIndex).getAsJsonObject();
String uri = getAsString(image, "uri");
+ Integer bufferView = getAsInteger(image, "bufferView");
+ String mimeType = getAsString(image, "mimeType");
Texture2D result;
if (uri == null) {
- //Image is embed in a buffer not supported yet
- //TODO support images embed in a buffer
- throw new AssetLoadException("Images embed in a buffer are not supported yet");
+ assertNotNull(bufferView, "Image " + sourceIndex + " should either have an uri or a bufferView");
+ assertNotNull(mimeType, "Image " + sourceIndex + " should have a mimeType");
+ byte[] data = (byte[]) readBuffer(bufferView, 0, -1, null, 1, VertexBuffer.Format.Byte);
+ String extension = mimeType.split("/")[1];
+ TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip);
+ result = (Texture2D) info.getManager().loadAssetFromStream(key, new ByteArrayInputStream(data));
+
} else if (uri.startsWith("data:")) {
//base64 encoded image
String[] uriInfo = uri.split(",");
@@ -672,11 +697,7 @@ public class GltfLoader implements AssetLoader {
Texture tex = info.getManager().loadTexture(key);
result = (Texture2D) tex;
}
-
- result = customContentManager.readExtensionAndExtras("image", image, result);
-
return result;
-
}
public void readAnimation(int animationIndex) throws IOException {
@@ -844,7 +865,7 @@ public class GltfLoader implements AssetLoader {
}
}
- public Texture2D readSampler(int samplerIndex, Texture2D texture) {
+ public Texture2D readSampler(int samplerIndex, Texture2D texture) throws IOException {
if (samplers == null) {
throw new AssetLoadException("No samplers defined");
}
@@ -1225,6 +1246,10 @@ public class GltfLoader implements AssetLoader {
}
}
+ private class TextureData {
+ byte[] data;
+ }
+
private interface Populator {
T populate(Integer bufferViewIndex, int componentType, String type, int count, int byteOffset, boolean normalized) throws IOException;
}
@@ -1380,5 +1405,6 @@ public class GltfLoader implements AssetLoader {
return new SkinBuffers(data, format.getComponentSize());
}
}
+
}
diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java
index 75d48d06c..b5fd0614a 100644
--- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java
+++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java
@@ -253,10 +253,11 @@ public class GltfUtils {
return;
}
LittleEndien stream = getStream(source);
- if (store instanceof short[]) {
+ if (store instanceof byte[]) {
+ populateByteArray((byte[]) store, stream, count, byteOffset, byteStride, numComponents, format);
+ } else if (store instanceof short[]) {
populateShortArray((short[]) store, stream, count, byteOffset, byteStride, numComponents, format);
- } else
- if (store instanceof float[]) {
+ } else if (store instanceof float[]) {
populateFloatArray((float[]) store, stream, count, byteOffset, byteStride, numComponents, format);
} else if (store instanceof Vector3f[]) {
populateVector3fArray((Vector3f[]) store, stream, count, byteOffset, byteStride, numComponents, format);
@@ -367,6 +368,26 @@ public class GltfUtils {
}
+ private static void populateByteArray(byte[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+ int componentSize = format.getComponentSize();
+ int index = byteOffset;
+ int dataLength = componentSize * numComponents;
+ int stride = Math.max(dataLength, byteStride);
+ int end = count * stride + byteOffset;
+ stream.skipBytes(byteOffset);
+ int arrayIndex = 0;
+ while (index < end) {
+ for (int i = 0; i < numComponents; i++) {
+ array[arrayIndex] = stream.readByte();
+ arrayIndex++;
+ }
+ if (dataLength < stride) {
+ stream.skipBytes(stride - dataLength);
+ }
+ index += stride;
+ }
+ }
+
private static void populateShortArray(short[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
int componentSize = format.getComponentSize();
int index = byteOffset;
diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRSpecGlossExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRSpecGlossExtensionLoader.java
index 2052c9a5d..f9a9c68ee 100644
--- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRSpecGlossExtensionLoader.java
+++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRSpecGlossExtensionLoader.java
@@ -3,6 +3,8 @@ package com.jme3.scene.plugins.gltf;
import com.google.gson.JsonElement;
import com.jme3.asset.AssetKey;
+import java.io.IOException;
+
import static com.jme3.scene.plugins.gltf.GltfUtils.getAsColor;
import static com.jme3.scene.plugins.gltf.GltfUtils.getAsFloat;
@@ -15,7 +17,7 @@ public class PBRSpecGlossExtensionLoader implements ExtensionLoader {
private PBRSpecGlossMaterialAdapter materialAdapter = new PBRSpecGlossMaterialAdapter();
@Override
- public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input) {
+ public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input) throws IOException {
MaterialAdapter adapter = materialAdapter;
AssetKey key = loader.getInfo().getKey();
//check for a custom adapter for spec/gloss pipeline