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