From 0a6e8741cf6038c294c434ecc92f054932a608f6 Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sat, 24 Mar 2018 14:44:15 -0400 Subject: [PATCH 01/54] Added ColorRGBA.fromIntABGR() as reciprocal to asIntABGR(). --- .../src/main/java/com/jme3/math/ColorRGBA.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java b/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java index 9180c69a6..f403bbaa6 100644 --- a/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java +++ b/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java @@ -558,6 +558,19 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable a = ((byte) (color) & 0xFF) / 255f; return this; } + /** + * Sets the RGBA values of this ColorRGBA with the given combined ABGR value + * Bits 24-31 are alpha, bits 16-23 are blue, bits 8-15 are green, bits 0-7 are red. + * @param color The integer ABGR value used to set this object. + * @return this + */ + public ColorRGBA fromIntABGR(int color) { + a = ((byte) (color >> 24) & 0xFF) / 255f; + b = ((byte) (color >> 16) & 0xFF) / 255f; + g = ((byte) (color >> 8) & 0xFF) / 255f; + r = ((byte) (color) & 0xFF) / 255f; + return this; + } /** * Transform this ColorRGBA to a Vector3f using From 0fb5eeddd7e917a5684a3980af26d911403f8be8 Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sat, 24 Mar 2018 14:45:11 -0400 Subject: [PATCH 02/54] Fixed an NPE in getNumElements() if the data field was null. --- jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java index 70d40a64c..001cf570d 100644 --- a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java +++ b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java @@ -606,6 +606,9 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { * @return The total number of data elements in the data buffer. */ public int getNumElements(){ + if( data == null ) { + return 0; + } int elements = data.limit() / components; if (format == Format.Half) elements /= 2; From c2b6e8f407a69ebb7ec9f796440d4783c9065da7 Mon Sep 17 00:00:00 2001 From: Nehon Date: Tue, 28 Nov 2017 20:45:46 +0100 Subject: [PATCH 03/54] Allows build of 3.2 branch --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index c24f8941b..00aed0854 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ branches: only: - master - v3.1 + - /^v3.2.0-.*$/ matrix: include: From 75f90fb70cb948e4bdc2c44ca284b6e4d8c3fe5c Mon Sep 17 00:00:00 2001 From: Nehon Date: Sun, 17 Dec 2017 18:02:59 +0100 Subject: [PATCH 04/54] New Armature system --- .../java/com/jme3/animation/Armature.java | 251 ++++++ .../com/jme3/animation/ArmatureControl.java | 742 ++++++++++++++++++ .../main/java/com/jme3/animation/Joint.java | 292 +++++++ .../main/java/com/jme3/math/Transform.java | 17 +- .../src/main/java/com/jme3/scene/Mesh.java | 29 +- 5 files changed, 1313 insertions(+), 18 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/animation/Armature.java create mode 100644 jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java create mode 100644 jme3-core/src/main/java/com/jme3/animation/Joint.java diff --git a/jme3-core/src/main/java/com/jme3/animation/Armature.java b/jme3-core/src/main/java/com/jme3/animation/Armature.java new file mode 100644 index 000000000..9c09e63a0 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/animation/Armature.java @@ -0,0 +1,251 @@ +package com.jme3.animation; + +import com.jme3.export.*; +import com.jme3.math.Matrix4f; +import com.jme3.util.TempVars; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Nehon on 15/12/2017. + */ +public class Armature implements JmeCloneable, Savable { + + private Joint[] rootJoints; + private Joint[] jointList; + + /** + * Contains the skinning matrices, multiplying it by a vertex effected by a bone + * will cause it to go to the animated position. + */ + private transient Matrix4f[] skinningMatrixes; + + + /** + * Serialization only + */ + public Armature() { + } + + /** + * Creates an armature from a joint list. + * The root joints are found automatically. + *

+ * Note that using this constructor will cause the joints in the list + * to have their bind pose recomputed based on their local transforms. + * + * @param jointList The list of joints to manage by this Armature + */ + public Armature(Joint[] jointList) { + this.jointList = jointList; + + List rootJointList = new ArrayList<>(); + for (int i = jointList.length - 1; i >= 0; i--) { + Joint b = jointList[i]; + if (b.getParent() == null) { + rootJointList.add(b); + } + } + rootJoints = rootJointList.toArray(new Joint[rootJointList.size()]); + + createSkinningMatrices(); + + for (int i = rootJoints.length - 1; i >= 0; i--) { + Joint rootJoint = rootJoints[i]; + rootJoint.update(); + } + } + +// +// /** +// * Special-purpose copy constructor. +// *

+// * Shallow copies bind pose data from the source skeleton, does not +// * copy any other data. +// * +// * @param source The source Skeleton to copy from +// */ +// public Armature(Armature source) { +// Joint[] sourceList = source.jointList; +// jointList = new Joint[sourceList.length]; +// for (int i = 0; i < sourceList.length; i++) { +// jointList[i] = new Joint(sourceList[i]); +// } +// +// rootJoints = new Bone[source.rootJoints.length]; +// for (int i = 0; i < rootJoints.length; i++) { +// rootJoints[i] = recreateBoneStructure(source.rootJoints[i]); +// } +// createSkinningMatrices(); +// +// for (int i = rootJoints.length - 1; i >= 0; i--) { +// rootJoints[i].update(); +// } +// } + + /** + * Update all joints sin this Amature. + */ + public void update() { + for (Joint rootJoint : rootJoints) { + rootJoint.update(); + } + } + + private void createSkinningMatrices() { + skinningMatrixes = new Matrix4f[jointList.length]; + for (int i = 0; i < skinningMatrixes.length; i++) { + skinningMatrixes[i] = new Matrix4f(); + } + } + + /** + * returns the array of all root joints of this Armatire + * + * @return + */ + public Joint[] getRoots() { + return rootJoints; + } + + /** + * return a joint for the given index + * + * @param index + * @return + */ + public Joint getJoint(int index) { + return jointList[index]; + } + + /** + * returns the joint with the given name + * + * @param name + * @return + */ + public Joint getJoint(String name) { + for (int i = 0; i < jointList.length; i++) { + if (jointList[i].getName().equals(name)) { + return jointList[i]; + } + } + return null; + } + + /** + * returns the bone index of the given bone + * + * @param joint + * @return + */ + public int getJointIndex(Joint joint) { + for (int i = 0; i < jointList.length; i++) { + if (jointList[i] == joint) { + return i; + } + } + + return -1; + } + + /** + * returns the joint index of the joint that has the given name + * + * @param name + * @return + */ + public int getJointIndex(String name) { + for (int i = 0; i < jointList.length; i++) { + if (jointList[i].getName().equals(name)) { + return i; + } + } + + return -1; + } + + /** + * Saves the current Armature state as it's bind pose. + */ + public void setBindPose() { + //make sure all bones are updated + update(); + //Save the current pose as bind pose + for (Joint rootJoint : rootJoints) { + rootJoint.setBindPose(); + } + } + + /** + * Compute the skining matrices for each bone of the armature that would be used to transform vertices of associated meshes + * + * @return + */ + public Matrix4f[] computeSkinningMatrices() { + TempVars vars = TempVars.get(); + for (int i = 0; i < jointList.length; i++) { + jointList[i].getOffsetTransform(skinningMatrixes[i], vars.quat1, vars.vect1, vars.vect2, vars.tempMat3); + } + vars.release(); + return skinningMatrixes; + } + + /** + * returns the number of joints of this armature + * + * @return + */ + public int getJointCount() { + return jointList.length; + } + + @Override + public Object jmeClone() { + try { + Armature clone = (Armature) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + this.rootJoints = cloner.clone(rootJoints); + this.jointList = cloner.clone(jointList); + this.skinningMatrixes = cloner.clone(skinningMatrixes); + } + + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule input = im.getCapsule(this); + + Savable[] jointRootsAsSavable = input.readSavableArray("rootJoints", null); + rootJoints = new Joint[jointRootsAsSavable.length]; + System.arraycopy(jointRootsAsSavable, 0, rootJoints, 0, jointRootsAsSavable.length); + + Savable[] jointListAsSavable = input.readSavableArray("jointList", null); + jointList = new Joint[jointListAsSavable.length]; + System.arraycopy(jointListAsSavable, 0, jointList, 0, jointListAsSavable.length); + + createSkinningMatrices(); + + for (Joint rootJoint : rootJoints) { + rootJoint.update(); + } + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule output = ex.getCapsule(this); + output.write(rootJoints, "rootJoints", null); + output.write(jointList, "jointList", null); + } + +} diff --git a/jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java b/jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java new file mode 100644 index 000000000..00f017f32 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java @@ -0,0 +1,742 @@ +/* + * Copyright (c) 2009-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.animation; + +import com.jme3.export.*; +import com.jme3.material.MatParamOverride; +import com.jme3.math.FastMath; +import com.jme3.math.Matrix4f; +import com.jme3.renderer.*; +import com.jme3.scene.*; +import com.jme3.scene.VertexBuffer.Type; +import com.jme3.scene.control.AbstractControl; +import com.jme3.scene.mesh.IndexBuffer; +import com.jme3.shader.VarType; +import com.jme3.util.SafeArrayList; +import com.jme3.util.TempVars; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; +import java.nio.Buffer; +import java.nio.FloatBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * The Skeleton control deforms a model according to a armature, It handles the + * computation of the deformation matrices and performs the transformations on + * the mesh + * + * @author Rémy Bouquet Based on AnimControl by Kirill Vainer + */ +public class ArmatureControl extends AbstractControl implements Cloneable, JmeCloneable { + + private static final Logger logger = Logger.getLogger(ArmatureControl.class.getName()); + + /** + * The armature of the model. + */ + private Armature armature; + + /** + * List of geometries affected by this control. + */ + private SafeArrayList targets = new SafeArrayList(Geometry.class); + + /** + * Used to track when a mesh was updated. Meshes are only updated if they + * are visible in at least one camera. + */ + private boolean wasMeshUpdated = false; + + /** + * User wishes to use hardware skinning if available. + */ + private transient boolean hwSkinningDesired = true; + + /** + * Hardware skinning is currently being used. + */ + private transient boolean hwSkinningEnabled = false; + + /** + * Hardware skinning was tested on this GPU, results + * are stored in {@link #hwSkinningSupported} variable. + */ + private transient boolean hwSkinningTested = false; + + /** + * If hardware skinning was {@link #hwSkinningTested tested}, then + * this variable will be set to true if supported, and false if otherwise. + */ + private transient boolean hwSkinningSupported = false; + + /** + * Bone offset matrices, recreated each frame + */ + private transient Matrix4f[] offsetMatrices; + + + private MatParamOverride numberOfJointsParam; + private MatParamOverride jointMatricesParam; + + /** + * Serialization only. Do not use. + */ + public ArmatureControl() { + } + + /** + * Creates a armature control. The list of targets will be acquired + * automatically when the control is attached to a node. + * + * @param skeleton the armature + */ + public ArmatureControl(Armature armature) { + if (armature == null) { + throw new IllegalArgumentException("armature cannot be null"); + } + this.armature = armature; + this.numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null); + this.jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null); + } + + + private void switchToHardware() { + numberOfJointsParam.setEnabled(true); + jointMatricesParam.setEnabled(true); + + // Next full 10 bones (e.g. 30 on 24 bones) + int numBones = ((armature.getJointCount() / 10) + 1) * 10; + numberOfJointsParam.setValue(numBones); + + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { + mesh.prepareForAnim(false); + } + } + } + + private void switchToSoftware() { + numberOfJointsParam.setEnabled(false); + jointMatricesParam.setEnabled(false); + + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { + mesh.prepareForAnim(true); + } + } + } + + private boolean testHardwareSupported(RenderManager rm) { + + //Only 255 bones max supported with hardware skinning + if (armature.getJointCount() > 255) { + return false; + } + + switchToHardware(); + + try { + rm.preloadScene(spatial); + return true; + } catch (RendererException e) { + logger.log(Level.WARNING, "Could not enable HW skinning due to shader compile error:", e); + return false; + } + } + + /** + * Specifies if hardware skinning is preferred. If it is preferred and + * supported by GPU, it shall be enabled, if its not preferred, or not + * supported by GPU, then it shall be disabled. + * + * @param preferred + * @see #isHardwareSkinningUsed() + */ + public void setHardwareSkinningPreferred(boolean preferred) { + hwSkinningDesired = preferred; + } + + /** + * @return True if hardware skinning is preferable to software skinning. + * Set to false by default. + * @see #setHardwareSkinningPreferred(boolean) + */ + public boolean isHardwareSkinningPreferred() { + return hwSkinningDesired; + } + + /** + * @return True is hardware skinning is activated and is currently used, false otherwise. + */ + public boolean isHardwareSkinningUsed() { + return hwSkinningEnabled; + } + + + /** + * If specified the geometry has an animated mesh, add its mesh and material + * to the lists of animation targets. + */ + private void findTargets(Geometry geometry) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { + targets.add(geometry); + } + + } + + private void findTargets(Node node) { + for (Spatial child : node.getChildren()) { + if (child instanceof Geometry) { + findTargets((Geometry) child); + } else if (child instanceof Node) { + findTargets((Node) child); + } + } + } + + @Override + public void setSpatial(Spatial spatial) { + Spatial oldSpatial = this.spatial; + super.setSpatial(spatial); + updateTargetsAndMaterials(spatial); + + if (oldSpatial != null) { + oldSpatial.removeMatParamOverride(numberOfJointsParam); + oldSpatial.removeMatParamOverride(jointMatricesParam); + } + + if (spatial != null) { + spatial.removeMatParamOverride(numberOfJointsParam); + spatial.removeMatParamOverride(jointMatricesParam); + spatial.addMatParamOverride(numberOfJointsParam); + spatial.addMatParamOverride(jointMatricesParam); + } + } + + private void controlRenderSoftware() { + resetToBind(); // reset morph meshes to bind pose + + offsetMatrices = armature.computeSkinningMatrices(); + + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + // NOTE: This assumes code higher up has + // already ensured this mesh is animated. + // Otherwise a crash will happen in skin update. + softwareSkinUpdate(mesh, offsetMatrices); + } + } + + private void controlRenderHardware() { + offsetMatrices = armature.computeSkinningMatrices(); + jointMatricesParam.setValue(offsetMatrices); + } + + @Override + protected void controlRender(RenderManager rm, ViewPort vp) { + if (!wasMeshUpdated) { + updateTargetsAndMaterials(spatial); + + // Prevent illegal cases. These should never happen. + assert hwSkinningTested || (!hwSkinningTested && !hwSkinningSupported && !hwSkinningEnabled); + assert !hwSkinningEnabled || (hwSkinningEnabled && hwSkinningTested && hwSkinningSupported); + + if (hwSkinningDesired && !hwSkinningTested) { + hwSkinningTested = true; + hwSkinningSupported = testHardwareSupported(rm); + + if (hwSkinningSupported) { + hwSkinningEnabled = true; + + Logger.getLogger(ArmatureControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for {0}", spatial); + } else { + switchToSoftware(); + } + } else if (hwSkinningDesired && hwSkinningSupported && !hwSkinningEnabled) { + switchToHardware(); + hwSkinningEnabled = true; + } else if (!hwSkinningDesired && hwSkinningEnabled) { + switchToSoftware(); + hwSkinningEnabled = false; + } + + if (hwSkinningEnabled) { + controlRenderHardware(); + } else { + controlRenderSoftware(); + } + + wasMeshUpdated = true; + } + } + + @Override + protected void controlUpdate(float tpf) { + wasMeshUpdated = false; + } + + //only do this for software updates + void resetToBind() { + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { + Buffer bwBuff = mesh.getBuffer(Type.BoneWeight).getData(); + Buffer biBuff = mesh.getBuffer(Type.BoneIndex).getData(); + if (!biBuff.hasArray() || !bwBuff.hasArray()) { + mesh.prepareForAnim(true); // prepare for software animation + } + VertexBuffer bindPos = mesh.getBuffer(Type.BindPosePosition); + VertexBuffer bindNorm = mesh.getBuffer(Type.BindPoseNormal); + VertexBuffer pos = mesh.getBuffer(Type.Position); + VertexBuffer norm = mesh.getBuffer(Type.Normal); + FloatBuffer pb = (FloatBuffer) pos.getData(); + FloatBuffer nb = (FloatBuffer) norm.getData(); + FloatBuffer bpb = (FloatBuffer) bindPos.getData(); + FloatBuffer bnb = (FloatBuffer) bindNorm.getData(); + pb.clear(); + nb.clear(); + bpb.clear(); + bnb.clear(); + + //reseting bind tangents if there is a bind tangent buffer + VertexBuffer bindTangents = mesh.getBuffer(Type.BindPoseTangent); + if (bindTangents != null) { + VertexBuffer tangents = mesh.getBuffer(Type.Tangent); + FloatBuffer tb = (FloatBuffer) tangents.getData(); + FloatBuffer btb = (FloatBuffer) bindTangents.getData(); + tb.clear(); + btb.clear(); + tb.put(btb).clear(); + } + + + pb.put(bpb).clear(); + nb.put(bnb).clear(); + } + } + } + + @Override + public Object jmeClone() { + return super.jmeClone(); + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + super.cloneFields(cloner, original); + + this.armature = cloner.clone(armature); + + // If the targets were cloned then this will clone them. If the targets + // were shared then this will share them. + this.targets = cloner.clone(targets); + + this.numberOfJointsParam = cloner.clone(numberOfJointsParam); + this.jointMatricesParam = cloner.clone(jointMatricesParam); + } + + /** + * Access the attachments node of the named bone. If the bone doesn't + * already have an attachments node, create one and attach it to the scene + * graph. Models and effects attached to the attachments node will follow + * the bone's motions. + * + * @param jointName the name of the joint + * @return the attachments node of the joint + */ + public Node getAttachmentsNode(String jointName) { + Joint b = armature.getJoint(jointName); + if (b == null) { + throw new IllegalArgumentException("Given bone name does not exist " + + "in the armature."); + } + + updateTargetsAndMaterials(spatial); + int boneIndex = armature.getJointIndex(b); + Node n = b.getAttachmentsNode(boneIndex, targets); + /* + * Select a node to parent the attachments node. + */ + Node parent; + if (spatial instanceof Node) { + parent = (Node) spatial; // the usual case + } else { + parent = spatial.getParent(); + } + parent.attachChild(n); + + return n; + } + + /** + * returns the armature of this control + * + * @return + */ + public Armature getArmature() { + return armature; + } + + /** + * Enumerate the target meshes of this control. + * + * @return a new array + */ + public Mesh[] getTargets() { + Mesh[] result = new Mesh[targets.size()]; + int i = 0; + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + result[i] = mesh; + i++; + } + + return result; + } + + /** + * Update the mesh according to the given transformation matrices + * + * @param mesh then mesh + * @param offsetMatrices the transformation matrices to apply + */ + private void softwareSkinUpdate(Mesh mesh, Matrix4f[] offsetMatrices) { + + VertexBuffer tb = mesh.getBuffer(Type.Tangent); + if (tb == null) { + //if there are no tangents use the classic skinning + applySkinning(mesh, offsetMatrices); + } else { + //if there are tangents use the skinning with tangents + applySkinningTangents(mesh, offsetMatrices, tb); + } + + + } + + /** + * Method to apply skinning transforms to a mesh's buffers + * + * @param mesh the mesh + * @param offsetMatrices the offset matices to apply + */ + private void applySkinning(Mesh mesh, Matrix4f[] offsetMatrices) { + int maxWeightsPerVert = mesh.getMaxNumWeights(); + if (maxWeightsPerVert <= 0) { + throw new IllegalStateException("Max weights per vert is incorrectly set!"); + } + int fourMinusMaxWeights = 4 - maxWeightsPerVert; + + // NOTE: This code assumes the vertex buffer is in bind pose + // resetToBind() has been called this frame + VertexBuffer vb = mesh.getBuffer(Type.Position); + FloatBuffer fvb = (FloatBuffer) vb.getData(); + fvb.rewind(); + + VertexBuffer nb = mesh.getBuffer(Type.Normal); + FloatBuffer fnb = (FloatBuffer) nb.getData(); + fnb.rewind(); + + // get boneIndexes and weights for mesh + IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData()); + FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData(); + + wb.rewind(); + + float[] weights = wb.array(); + int idxWeights = 0; + + TempVars vars = TempVars.get(); + + float[] posBuf = vars.skinPositions; + float[] normBuf = vars.skinNormals; + + int iterations = (int) FastMath.ceil(fvb.limit() / ((float) posBuf.length)); + int bufLength = posBuf.length; + for (int i = iterations - 1; i >= 0; i--) { + // read next set of positions and normals from native buffer + bufLength = Math.min(posBuf.length, fvb.remaining()); + fvb.get(posBuf, 0, bufLength); + fnb.get(normBuf, 0, bufLength); + int verts = bufLength / 3; + int idxPositions = 0; + + // iterate vertices and apply skinning transform for each effecting bone + for (int vert = verts - 1; vert >= 0; vert--) { + // Skip this vertex if the first weight is zero. + if (weights[idxWeights] == 0) { + idxPositions += 3; + idxWeights += 4; + continue; + } + + float nmx = normBuf[idxPositions]; + float vtx = posBuf[idxPositions++]; + float nmy = normBuf[idxPositions]; + float vty = posBuf[idxPositions++]; + float nmz = normBuf[idxPositions]; + float vtz = posBuf[idxPositions++]; + + float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0; + + for (int w = maxWeightsPerVert - 1; w >= 0; w--) { + float weight = weights[idxWeights]; + Matrix4f mat = offsetMatrices[ib.get(idxWeights++)]; + + rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight; + ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight; + rz += (mat.m20 * vtx + mat.m21 * vty + mat.m22 * vtz + mat.m23) * weight; + + rnx += (nmx * mat.m00 + nmy * mat.m01 + nmz * mat.m02) * weight; + rny += (nmx * mat.m10 + nmy * mat.m11 + nmz * mat.m12) * weight; + rnz += (nmx * mat.m20 + nmy * mat.m21 + nmz * mat.m22) * weight; + } + + idxWeights += fourMinusMaxWeights; + + idxPositions -= 3; + normBuf[idxPositions] = rnx; + posBuf[idxPositions++] = rx; + normBuf[idxPositions] = rny; + posBuf[idxPositions++] = ry; + normBuf[idxPositions] = rnz; + posBuf[idxPositions++] = rz; + } + + fvb.position(fvb.position() - bufLength); + fvb.put(posBuf, 0, bufLength); + fnb.position(fnb.position() - bufLength); + fnb.put(normBuf, 0, bufLength); + } + + vars.release(); + + vb.updateData(fvb); + nb.updateData(fnb); + + } + + /** + * Specific method for skinning with tangents to avoid cluttering the + * classic skinning calculation with null checks that would slow down the + * process even if tangents don't have to be computed. Also the iteration + * has additional indexes since tangent has 4 components instead of 3 for + * pos and norm + * + * @param maxWeightsPerVert maximum number of weights per vertex + * @param mesh the mesh + * @param offsetMatrices the offsetMaytrices to apply + * @param tb the tangent vertexBuffer + */ + private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexBuffer tb) { + int maxWeightsPerVert = mesh.getMaxNumWeights(); + + if (maxWeightsPerVert <= 0) { + throw new IllegalStateException("Max weights per vert is incorrectly set!"); + } + + int fourMinusMaxWeights = 4 - maxWeightsPerVert; + + // NOTE: This code assumes the vertex buffer is in bind pose + // resetToBind() has been called this frame + VertexBuffer vb = mesh.getBuffer(Type.Position); + FloatBuffer fvb = (FloatBuffer) vb.getData(); + fvb.rewind(); + + VertexBuffer nb = mesh.getBuffer(Type.Normal); + + FloatBuffer fnb = (FloatBuffer) nb.getData(); + fnb.rewind(); + + + FloatBuffer ftb = (FloatBuffer) tb.getData(); + ftb.rewind(); + + + // get boneIndexes and weights for mesh + IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData()); + FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData(); + + wb.rewind(); + + float[] weights = wb.array(); + int idxWeights = 0; + + TempVars vars = TempVars.get(); + + + float[] posBuf = vars.skinPositions; + float[] normBuf = vars.skinNormals; + float[] tanBuf = vars.skinTangents; + + int iterations = (int) FastMath.ceil(fvb.limit() / ((float) posBuf.length)); + int bufLength = 0; + int tanLength = 0; + for (int i = iterations - 1; i >= 0; i--) { + // read next set of positions and normals from native buffer + bufLength = Math.min(posBuf.length, fvb.remaining()); + tanLength = Math.min(tanBuf.length, ftb.remaining()); + fvb.get(posBuf, 0, bufLength); + fnb.get(normBuf, 0, bufLength); + ftb.get(tanBuf, 0, tanLength); + int verts = bufLength / 3; + int idxPositions = 0; + //tangents has their own index because of the 4 components + int idxTangents = 0; + + // iterate vertices and apply skinning transform for each effecting bone + for (int vert = verts - 1; vert >= 0; vert--) { + // Skip this vertex if the first weight is zero. + if (weights[idxWeights] == 0) { + idxTangents += 4; + idxPositions += 3; + idxWeights += 4; + continue; + } + + float nmx = normBuf[idxPositions]; + float vtx = posBuf[idxPositions++]; + float nmy = normBuf[idxPositions]; + float vty = posBuf[idxPositions++]; + float nmz = normBuf[idxPositions]; + float vtz = posBuf[idxPositions++]; + + float tnx = tanBuf[idxTangents++]; + float tny = tanBuf[idxTangents++]; + float tnz = tanBuf[idxTangents++]; + + // skipping the 4th component of the tangent since it doesn't have to be transformed + idxTangents++; + + float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0, rtx = 0, rty = 0, rtz = 0; + + for (int w = maxWeightsPerVert - 1; w >= 0; w--) { + float weight = weights[idxWeights]; + Matrix4f mat = offsetMatrices[ib.get(idxWeights++)]; + + rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight; + ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight; + rz += (mat.m20 * vtx + mat.m21 * vty + mat.m22 * vtz + mat.m23) * weight; + + rnx += (nmx * mat.m00 + nmy * mat.m01 + nmz * mat.m02) * weight; + rny += (nmx * mat.m10 + nmy * mat.m11 + nmz * mat.m12) * weight; + rnz += (nmx * mat.m20 + nmy * mat.m21 + nmz * mat.m22) * weight; + + rtx += (tnx * mat.m00 + tny * mat.m01 + tnz * mat.m02) * weight; + rty += (tnx * mat.m10 + tny * mat.m11 + tnz * mat.m12) * weight; + rtz += (tnx * mat.m20 + tny * mat.m21 + tnz * mat.m22) * weight; + } + + idxWeights += fourMinusMaxWeights; + + idxPositions -= 3; + + normBuf[idxPositions] = rnx; + posBuf[idxPositions++] = rx; + normBuf[idxPositions] = rny; + posBuf[idxPositions++] = ry; + normBuf[idxPositions] = rnz; + posBuf[idxPositions++] = rz; + + idxTangents -= 4; + + tanBuf[idxTangents++] = rtx; + tanBuf[idxTangents++] = rty; + tanBuf[idxTangents++] = rtz; + + //once again skipping the 4th component of the tangent + idxTangents++; + } + + fvb.position(fvb.position() - bufLength); + fvb.put(posBuf, 0, bufLength); + fnb.position(fnb.position() - bufLength); + fnb.put(normBuf, 0, bufLength); + ftb.position(ftb.position() - tanLength); + ftb.put(tanBuf, 0, tanLength); + } + + vars.release(); + + vb.updateData(fvb); + nb.updateData(fnb); + tb.updateData(ftb); + } + + @Override + public void write(JmeExporter ex) throws IOException { + super.write(ex); + OutputCapsule oc = ex.getCapsule(this); + oc.write(armature, "armature", null); + + oc.write(numberOfJointsParam, "numberOfBonesParam", null); + oc.write(jointMatricesParam, "boneMatricesParam", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule in = im.getCapsule(this); + armature = (Armature) in.readSavable("armature", null); + + numberOfJointsParam = (MatParamOverride) in.readSavable("numberOfBonesParam", null); + jointMatricesParam = (MatParamOverride) in.readSavable("boneMatricesParam", null); + + if (numberOfJointsParam == null) { + numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null); + jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null); + getSpatial().addMatParamOverride(numberOfJointsParam); + getSpatial().addMatParamOverride(jointMatricesParam); + } + } + + /** + * Update the lists of animation targets. + * + * @param spatial the controlled spatial + */ + private void updateTargetsAndMaterials(Spatial spatial) { + targets.clear(); + + if (spatial instanceof Node) { + findTargets((Node) spatial); + } else if (spatial instanceof Geometry) { + findTargets((Geometry) spatial); + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/animation/Joint.java b/jme3-core/src/main/java/com/jme3/animation/Joint.java new file mode 100644 index 000000000..a5a35af7f --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/animation/Joint.java @@ -0,0 +1,292 @@ +package com.jme3.animation; + +import com.jme3.export.*; +import com.jme3.material.MatParamOverride; +import com.jme3.math.*; +import com.jme3.scene.*; +import com.jme3.shader.VarType; +import com.jme3.util.SafeArrayList; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * A Joint is the basic component of an armature designed to perform skeletal animation + * Created by Nehon on 15/12/2017. + */ +public class Joint implements Savable, JmeCloneable { + + private String name; + private Joint parent; + private ArrayList children = new ArrayList<>(); + private Geometry targetGeometry; + + public Joint() { + } + + public Joint(String name) { + this.name = name; + } + + /** + * The attachment node. + */ + private Node attachedNode; + + /** + * The transform of the joint in local space. Relative to its parent. + * Or relative to the model's origin for the root joint. + */ + private Transform localTransform = new Transform(); + + /** + * The base transform of the joint in local space. + * Those transform are the bone's initial value. + */ + private Transform baseLocalTransform = new Transform(); + + /** + * The transform of the bone in model space. Relative to the origin of the model. + */ + private Transform modelTransform = new Transform(); + + /** + * The matrix used to transform affected vertices position into the bone model space. + * Used for skinning. + */ + private Matrix4f inverseModelBindMatrix = new Matrix4f(); + + /** + * Updates world transforms for this bone and it's children. + */ + public final void update() { + this.updateModelTransforms(); + + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).update(); + } + } + + /** + * Updates the model transforms for this bone, and, possibly the attach node + * if not null. + *

+ * The model transform of this bone is computed by combining the parent's + * model transform with this bones' local transform. + */ + public final void updateModelTransforms() { + modelTransform.set(localTransform); + if (parent != null) { + modelTransform.combineWithParent(parent.getModelTransform()); + } + + updateAttachNode(); + } + + /** + * Update the local transform of the attachments node. + */ + private void updateAttachNode() { + if (attachedNode == null) { + return; + } + Node attachParent = attachedNode.getParent(); + if (attachParent == null || targetGeometry == null + || targetGeometry.getParent() == attachParent + && targetGeometry.getLocalTransform().isIdentity()) { + /* + * The animated meshes are in the same coordinate system as the + * attachments node: no further transforms are needed. + */ + attachedNode.setLocalTransform(modelTransform); + + } else { + Spatial loopSpatial = targetGeometry; + Transform combined = modelTransform.clone(); + /* + * Climb the scene graph applying local transforms until the + * attachments node's parent is reached. + */ + while (loopSpatial != attachParent && loopSpatial != null) { + Transform localTransform = loopSpatial.getLocalTransform(); + combined.combineWithParent(localTransform); + loopSpatial = loopSpatial.getParent(); + } + attachedNode.setLocalTransform(combined); + } + } + + /** + * Stores the skinning transform in the specified Matrix4f. + * The skinning transform applies the animation of the bone to a vertex. + *

+ * This assumes that the world transforms for the entire bone hierarchy + * have already been computed, otherwise this method will return undefined + * results. + * + * @param outTransform + */ + void getOffsetTransform(Matrix4f outTransform, Quaternion tmp1, Vector3f tmp2, Vector3f tmp3, Matrix3f tmp4) { + modelTransform.toTransformMatrix(outTransform).mult(inverseModelBindMatrix, outTransform); + } + + protected void setBindPose() { + //Note that the whole Armature must be updated before calling this method. + modelTransform.toTransformMatrix(inverseModelBindMatrix); + inverseModelBindMatrix.invertLocal(); + } + + public Vector3f getLocalTranslation() { + return localTransform.getTranslation(); + } + + public Quaternion getLocalRotation() { + return localTransform.getRotation(); + } + + public Vector3f getLocalScale() { + return localTransform.getScale(); + } + + public void setLocalTranslation(Vector3f translation) { + localTransform.setTranslation(translation); + } + + public void setLocalRotation(Quaternion rotation) { + localTransform.setRotation(rotation); + } + + public void setLocalScale(Vector3f scale) { + localTransform.setScale(scale); + } + + public void addChild(Joint child) { + children.add(child); + child.parent = this; + } + + public void setName(String name) { + this.name = name; + } + + public void setLocalTransform(Transform localTransform) { + this.localTransform.set(localTransform); + } + + public void setInverseModelBindMatrix(Matrix4f inverseModelBindMatrix) { + this.inverseModelBindMatrix = inverseModelBindMatrix; + } + + public String getName() { + return name; + } + + public Joint getParent() { + return parent; + } + + public ArrayList getChildren() { + return children; + } + + /** + * Access the attachments node of this joint. If this joint doesn't already + * have an attachments node, create one. Models and effects attached to the + * attachments node will follow this bone's motions. + * + * @param jointIndex this bone's index in its armature (≥0) + * @param targets a list of geometries animated by this bone's skeleton (not + * null, unaffected) + */ + Node getAttachmentsNode(int jointIndex, SafeArrayList targets) { + targetGeometry = null; + /* + * Search for a geometry animated by this particular bone. + */ + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimatedByJoint(jointIndex)) { + targetGeometry = geometry; + break; + } + } + + if (attachedNode == null) { + attachedNode = new Node(name + "_attachnode"); + attachedNode.setUserData("AttachedBone", this); + //We don't want the node to have a numBone set by a parent node so we force it to null + attachedNode.addMatParamOverride(new MatParamOverride(VarType.Int, "NumberOfBones", null)); + } + + return attachedNode; + } + + + public Transform getLocalTransform() { + return localTransform; + } + + public Transform getModelTransform() { + return modelTransform; + } + + public Matrix4f getInverseModelBindMatrix() { + return inverseModelBindMatrix; + } + + @Override + public Object jmeClone() { + try { + Joint clone = (Joint) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + this.children = cloner.clone(children); + this.attachedNode = cloner.clone(attachedNode); + this.targetGeometry = cloner.clone(targetGeometry); + + this.baseLocalTransform = cloner.clone(baseLocalTransform); + this.localTransform = cloner.clone(baseLocalTransform); + this.modelTransform = cloner.clone(baseLocalTransform); + this.inverseModelBindMatrix = cloner.clone(inverseModelBindMatrix); + } + + + @Override + @SuppressWarnings("unchecked") + public void read(JmeImporter im) throws IOException { + InputCapsule input = im.getCapsule(this); + + name = input.readString("name", null); + attachedNode = (Node) input.readSavable("attachedNode", null); + targetGeometry = (Geometry) input.readSavable("targetGeometry", null); + baseLocalTransform = (Transform) input.readSavable("baseLocalTransforms", baseLocalTransform); + localTransform.set(baseLocalTransform); + inverseModelBindMatrix = (Matrix4f) input.readSavable("inverseModelBindMatrix", inverseModelBindMatrix); + + ArrayList childList = input.readSavableArrayList("children", null); + for (int i = childList.size() - 1; i >= 0; i--) { + this.addChild(childList.get(i)); + } + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule output = ex.getCapsule(this); + + output.write(name, "name", null); + output.write(attachedNode, "attachedNode", null); + output.write(targetGeometry, "targetGeometry", null); + output.write(baseLocalTransform, "baseLocalTransform", new Transform()); + output.write(inverseModelBindMatrix, "inverseModelBindMatrix", new Matrix4f()); + output.writeSavableArrayList(children, "children", null); + } + +} diff --git a/jme3-core/src/main/java/com/jme3/math/Transform.java b/jme3-core/src/main/java/com/jme3/math/Transform.java index 6fb29c99b..f0a18bf91 100644 --- a/jme3-core/src/main/java/com/jme3/math/Transform.java +++ b/jme3-core/src/main/java/com/jme3/math/Transform.java @@ -32,6 +32,7 @@ package com.jme3.math; import com.jme3.export.*; + import java.io.IOException; /** @@ -257,11 +258,17 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable } public Matrix4f toTransformMatrix() { - Matrix4f trans = new Matrix4f(); - trans.setTranslation(translation); - trans.setRotationQuaternion(rot); - trans.setScale(scale); - return trans; + return toTransformMatrix(null); + } + + public Matrix4f toTransformMatrix(Matrix4f store) { + if (store == null) { + store = new Matrix4f(); + } + store.setTranslation(translation); + store.setRotationQuaternion(rot); + store.setScale(scale); + return store; } public void fromTransformMatrix(Matrix4f mat) { 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 e07c2cfa2..e9fccb73e 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java +++ b/jme3-core/src/main/java/com/jme3/scene/Mesh.java @@ -39,18 +39,11 @@ import com.jme3.collision.bih.BIHTree; import com.jme3.export.*; import com.jme3.material.Material; import com.jme3.material.RenderState; -import com.jme3.math.Matrix4f; -import com.jme3.math.Triangle; -import com.jme3.math.Vector2f; -import com.jme3.math.Vector3f; -import com.jme3.scene.VertexBuffer.Format; -import com.jme3.scene.VertexBuffer.Type; -import com.jme3.scene.VertexBuffer.Usage; +import com.jme3.math.*; +import com.jme3.scene.VertexBuffer.*; import com.jme3.scene.mesh.*; -import com.jme3.util.BufferUtils; -import com.jme3.util.IntMap; +import com.jme3.util.*; import com.jme3.util.IntMap.Entry; -import com.jme3.util.SafeArrayList; import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; @@ -1446,13 +1439,23 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { getBuffer(Type.HWBoneIndex) != null; } + /** + * @deprecated use isAnimatedByJoint + * @param boneIndex + * @return + */ + @Deprecated + public boolean isAnimatedByBone(int boneIndex) { + return isAnimatedByJoint(boneIndex); + } + /** * Test whether the specified bone animates this mesh. * - * @param boneIndex the bone's index in its skeleton + * @param jointIndex the bone's index in its skeleton * @return true if the specified bone animates this mesh, otherwise false */ - public boolean isAnimatedByBone(int boneIndex) { + public boolean isAnimatedByJoint(int jointIndex) { VertexBuffer biBuf = getBuffer(VertexBuffer.Type.BoneIndex); VertexBuffer wBuf = getBuffer(VertexBuffer.Type.BoneWeight); if (biBuf == null || wBuf == null) { @@ -1472,7 +1475,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { /* * Test each vertex to determine whether the bone affects it. */ - int biByte = boneIndex; + int biByte = jointIndex; for (int vIndex = 0; vIndex < numVertices; vIndex++) { for (int wIndex = 0; wIndex < 4; wIndex++) { int bIndex = boneIndexBuffer.get(); From 4904d0235e9eb69714276c413b19b932b33034a5 Mon Sep 17 00:00:00 2001 From: Nehon Date: Mon, 18 Dec 2017 17:41:13 +0100 Subject: [PATCH 05/54] Adds an ArmatureDebugger --- .../{SkeletonBone.java => ArmatureBone.java} | 159 +++---- .../debug/custom/ArmatureDebugAppState.java | 154 ++++++ ...tonDebugger.java => ArmatureDebugger.java} | 113 +++-- ...Wire.java => ArmatureInterJointsWire.java} | 60 ++- .../jme3/scene/debug/custom/BoneShape.java | 448 ++++-------------- .../debug/custom/SkeletonDebugAppState.java | 156 ------ .../com/jme3/scene/shape/AbstractBox.java | 22 +- .../main/java/com/jme3/scene/shape/Box.java | 9 +- .../java/com/jme3/scene/shape/StripBox.java | 9 +- .../Common/MatDefs/Misc/fakeLighting.j3md | 49 ++ .../ShaderNodes/Basic/Mat3Vec3Mult.j3sn | 31 ++ .../ShaderNodes/Basic/Mat3Vec3Mult100.frag | 3 + .../ShaderNodes/Misc/fakeLighting.j3sn | 33 ++ .../ShaderNodes/Misc/fakeLighting100.frag | 9 + .../jme3test/model/anim/TestArmature.java | 112 +++++ 15 files changed, 655 insertions(+), 712 deletions(-) rename jme3-core/src/main/java/com/jme3/scene/debug/custom/{SkeletonBone.java => ArmatureBone.java} (51%) create mode 100644 jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java rename jme3-core/src/main/java/com/jme3/scene/debug/custom/{SkeletonDebugger.java => ArmatureDebugger.java} (63%) rename jme3-core/src/main/java/com/jme3/scene/debug/custom/{SkeletonInterBoneWire.java => ArmatureInterJointsWire.java} (70%) delete mode 100644 jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java create mode 100644 jme3-core/src/main/resources/Common/MatDefs/Misc/fakeLighting.j3md create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Basic/Mat3Vec3Mult.j3sn create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Basic/Mat3Vec3Mult100.frag create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/fakeLighting.j3sn create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/fakeLighting100.frag create mode 100644 jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java similarity index 51% rename from jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java rename to jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java index b4f21ec54..b0b6e7e27 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java @@ -32,58 +32,51 @@ package com.jme3.scene.debug.custom; * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import java.util.Map; - -import com.jme3.animation.Bone; -import com.jme3.animation.Skeleton; +import com.jme3.animation.Armature; +import com.jme3.animation.Joint; import com.jme3.bounding.*; -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.Node; -import com.jme3.scene.VertexBuffer; +import com.jme3.scene.*; import com.jme3.scene.shape.Sphere; -import static com.jme3.util.BufferUtils.createFloatBuffer; - import java.nio.FloatBuffer; import java.util.HashMap; +import java.util.Map; + +import static com.jme3.util.BufferUtils.createFloatBuffer; /** * The class that displays either wires between the bones' heads if no length * data is supplied and full bones' shapes otherwise. */ -public class SkeletonBone extends Node { +public class ArmatureBone extends Node { /** - * The skeleton to be displayed. + * The armature to be displayed. */ - private Skeleton skeleton; + private Armature armature; /** * The map between the bone index and its length. */ - private Map boneNodes = new HashMap(); - private Map nodeBones = new HashMap(); + private Map jointNode = new HashMap<>(); + private Map nodeJoint = new HashMap<>(); private Node selectedNode = null; - private boolean guessBonesOrientation = false; + private boolean guessJointsOrientation = false; /** * Creates a wire with bone lengths data. If the data is supplied then the * wires will show each full bone (from head to tail). * - * @param skeleton the skeleton that will be shown + * @param armature the armature that will be shown * @param boneLengths a map between the bone's index and the bone's length */ - public SkeletonBone(Skeleton skeleton, Map boneLengths, boolean guessBonesOrientation) { - this.skeleton = skeleton; - this.skeleton.reset(); - this.skeleton.updateWorldVectors(); - this.guessBonesOrientation = guessBonesOrientation; - - BoneShape boneShape = new BoneShape(5, 12, 0.02f, 0.07f, 1f, false, false); - Sphere jointShape = new Sphere(10, 10, 0.1f); + public ArmatureBone(Armature armature, Map boneLengths, boolean guessJointsOrientation) { + this.armature = armature; + this.guessJointsOrientation = guessJointsOrientation; + + BoneShape boneShape = new BoneShape(); + Sphere jointShape = new Sphere(16, 16, 0.05f); jointShape.setBuffer(VertexBuffer.Type.Color, 4, createFloatBuffer(jointShape.getVertexCount() * 4)); FloatBuffer cb = jointShape.getFloatBuffer(VertexBuffer.Type.Color); @@ -92,13 +85,13 @@ public class SkeletonBone extends Node { cb.put(0.05f).put(0.05f).put(0.05f).put(1f); } - for (Bone bone : skeleton.getRoots()) { - createSkeletonGeoms(bone, boneShape, jointShape, boneLengths, skeleton, this, guessBonesOrientation); + for (Joint joint : armature.getRoots()) { + createSkeletonGeoms(joint, boneShape, jointShape, boneLengths, armature, this, guessJointsOrientation); } this.updateModelBound(); - Sphere originShape = new Sphere(10, 10, 0.02f); + Sphere originShape = new Sphere(16, 16, 0.02f); originShape.setBuffer(VertexBuffer.Type.Color, 4, createFloatBuffer(originShape.getVertexCount() * 4)); cb = originShape.getFloatBuffer(VertexBuffer.Type.Color); cb.rewind(); @@ -118,91 +111,65 @@ public class SkeletonBone extends Node { } origin.scale(scale); attachChild(origin); - - - } - protected final void createSkeletonGeoms(Bone bone, Mesh boneShape, Mesh jointShape, Map boneLengths, Skeleton skeleton, Node parent, boolean guessBonesOrientation) { + protected final void createSkeletonGeoms(Joint joint, Mesh boneShape, Mesh jointShape, Map boneLengths, Armature armature, Node parent, boolean guessBonesOrientation) { - if (guessBonesOrientation && bone.getName().equalsIgnoreCase("Site")) { - //BVH skeleton have a useless end point bone named Site - return; - } - Node n = new Node(bone.getName() + "Node"); - Geometry bGeom = new Geometry(bone.getName(), boneShape); - Geometry jGeom = new Geometry(bone.getName() + "Joint", jointShape); - n.setLocalTranslation(bone.getLocalPosition()); - n.setLocalRotation(bone.getLocalRotation()); + Node n = new Node(joint.getName() + "Node"); + Geometry bGeom = new Geometry(joint.getName() + "Bone", boneShape); + Geometry jGeom = new Geometry(joint.getName() + "Joint", jointShape); + n.setLocalTransform(joint.getLocalTransform()); - float boneLength = boneLengths.get(skeleton.getBoneIndex(bone)); - n.setLocalScale(bone.getLocalScale()); - - bGeom.setLocalRotation(new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X).normalizeLocal()); + float boneLength = boneLengths.get(armature.getJointIndex(joint)); if (guessBonesOrientation) { //One child only, the bone direction is from the parent joint to the child joint. - if (bone.getChildren().size() == 1) { - Vector3f v = bone.getChildren().get(0).getLocalPosition(); + if (joint.getChildren().size() == 1) { + Vector3f v = joint.getChildren().get(0).getLocalTranslation(); Quaternion q = new Quaternion(); q.lookAt(v, Vector3f.UNIT_Z); bGeom.setLocalRotation(q); - boneLength = v.length(); } //no child, the bone has the same direction as the parent bone. - if (bone.getChildren().isEmpty()) { - if (parent.getChildren().size() > 0) { - bGeom.setLocalRotation(parent.getChild(0).getLocalRotation()); - } else { - //no parent, let's use the bind orientation of the bone - bGeom.setLocalRotation(bone.getBindRotation()); + if (joint.getChildren().isEmpty()) { + //no parent, let's use the bind orientation of the bone + Spatial s = parent.getChild(0); + if (s != null) { + bGeom.setLocalRotation(s.getLocalRotation()); } } } - bGeom.setLocalScale(boneLength); - jGeom.setLocalScale(boneLength); - - n.attachChild(bGeom); - n.attachChild(jGeom); - //tip - if (bone.getChildren().size() != 1) { - Geometry gt = jGeom.clone(); - gt.scale(0.8f); - Vector3f v = new Vector3f(0, boneLength, 0); - if (guessBonesOrientation) { - if (bone.getChildren().isEmpty()) { - if (parent.getChildren().size() > 0) { - gt.setLocalTranslation(bGeom.getLocalRotation().mult(parent.getChild(0).getLocalRotation()).mult(v, v)); - } else { - gt.setLocalTranslation(bGeom.getLocalRotation().mult(bone.getBindRotation()).mult(v, v)); - } - } - } else { - gt.setLocalTranslation(v); - } + float boneScale = boneLength * 0.8f; + float scale = boneScale / 8f; + bGeom.setLocalScale(new Vector3f(scale, scale, boneScale)); + Vector3f offset = new Vector3f(0, 0, boneLength * 0.1f); + bGeom.getLocalRotation().multLocal(offset); + bGeom.setLocalTranslation(offset); + jGeom.setLocalScale(boneLength); - n.attachChild(gt); + if (joint.getChildren().size() <= 1) { + n.attachChild(bGeom); } + n.attachChild(jGeom); - - boneNodes.put(bone, n); - nodeBones.put(n, bone); + jointNode.put(joint, n); + nodeJoint.put(n, joint); parent.attachChild(n); - for (Bone childBone : bone.getChildren()) { - createSkeletonGeoms(childBone, boneShape, jointShape, boneLengths, skeleton, n, guessBonesOrientation); + for (Joint child : joint.getChildren()) { + createSkeletonGeoms(child, boneShape, jointShape, boneLengths, armature, n, guessBonesOrientation); } } - protected Bone select(Geometry g) { + protected Joint select(Geometry g) { Node parentNode = g.getParent(); if (parent != null) { - Bone b = nodeBones.get(parentNode); - if (b != null) { + Joint j = nodeJoint.get(parentNode); + if (j != null) { selectedNode = parentNode; } - return b; + return j; } return null; } @@ -212,17 +179,12 @@ public class SkeletonBone extends Node { } - protected final void updateSkeletonGeoms(Bone bone) { - if (guessBonesOrientation && bone.getName().equalsIgnoreCase("Site")) { - return; - } - Node n = boneNodes.get(bone); - n.setLocalTranslation(bone.getLocalPosition()); - n.setLocalRotation(bone.getLocalRotation()); - n.setLocalScale(bone.getLocalScale()); + protected final void updateSkeletonGeoms(Joint joint) { + Node n = jointNode.get(joint); + n.setLocalTransform(joint.getLocalTransform()); - for (Bone childBone : bone.getChildren()) { - updateSkeletonGeoms(childBone); + for (Joint child : joint.getChildren()) { + updateSkeletonGeoms(child); } } @@ -230,9 +192,8 @@ public class SkeletonBone extends Node { * The method updates the geometry according to the positions of the bones. */ public void updateGeometry() { - - for (Bone bone : skeleton.getRoots()) { - updateSkeletonGeoms(bone); + for (Joint joint : armature.getRoots()) { + updateSkeletonGeoms(joint); } } } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java new file mode 100644 index 000000000..73d0a7b01 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java @@ -0,0 +1,154 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.jme3.scene.debug.custom; + +import com.jme3.animation.*; +import com.jme3.app.Application; +import com.jme3.app.state.AbstractAppState; +import com.jme3.app.state.AppStateManager; +import com.jme3.input.MouseInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.light.DirectionalLight; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.*; + +import java.util.*; + +/** + * @author Nehon + */ +public class ArmatureDebugAppState extends AbstractAppState { + + private Node debugNode = new Node("debugNode"); + private Map armatures = new HashMap<>(); + private Map selectedBones = new HashMap<>(); + private Application app; + + @Override + public void initialize(AppStateManager stateManager, Application app) { + ViewPort vp = app.getRenderManager().createMainView("debug", app.getCamera()); + vp.attachScene(debugNode); + vp.setClearDepth(true); + this.app = app; + for (ArmatureDebugger armatureDebugger : armatures.values()) { + armatureDebugger.initialize(app.getAssetManager()); + } + app.getInputManager().addListener(actionListener, "shoot"); + app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); + super.initialize(stateManager, app); + + + debugNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal())); + + debugNode.addLight(new DirectionalLight(new Vector3f(1f, 1f, 1f).normalizeLocal(), new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f))); + + } + + @Override + public void update(float tpf) { + debugNode.updateLogicalState(tpf); + debugNode.updateGeometricState(); + } + + public ArmatureDebugger addArmature(ArmatureControl armatureControl, boolean guessJointsOrientation) { + Armature armature = armatureControl.getArmature(); + Spatial forSpatial = armatureControl.getSpatial(); + return addArmature(armature, forSpatial, guessJointsOrientation); + } + + public ArmatureDebugger addArmature(Armature armature, Spatial forSpatial, boolean guessJointsOrientation) { + + ArmatureDebugger ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature, guessJointsOrientation); + ad.setLocalTransform(forSpatial.getWorldTransform()); + if (forSpatial instanceof Node) { + List geoms = new ArrayList<>(); + findGeoms((Node) forSpatial, geoms); + if (geoms.size() == 1) { + ad.setLocalTransform(geoms.get(0).getWorldTransform()); + } + } + armatures.put(armature, ad); + debugNode.attachChild(ad); + if (isInitialized()) { + ad.initialize(app.getAssetManager()); + } + return ad; + } + + private void findGeoms(Node node, List geoms) { + for (Spatial spatial : node.getChildren()) { + if (spatial instanceof Geometry) { + geoms.add((Geometry) spatial); + } else if (spatial instanceof Node) { + findGeoms((Node) spatial, geoms); + } + } + } + + /** + * Pick a Target Using the Mouse Pointer.

  1. Map "pick target" action + * to a MouseButtonTrigger.
  2. flyCam.setEnabled(false); + *
  3. inputManager.setCursorVisible(true);
  4. Implement action in + * AnalogListener (TODO).
+ */ + private ActionListener actionListener = new ActionListener() { + public void onAction(String name, boolean isPressed, float tpf) { + //if (name.equals("shoot") && isPressed) { +// CollisionResults results = new CollisionResults(); +// Vector2f click2d = app.getInputManager().getCursorPosition(); +// Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); +// Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); +// Ray ray = new Ray(click3d, dir); +// +// debugNode.collideWith(ray, results); +// +// if (results.size() > 0) { +// // The closest result is the target that the player picked: +// Geometry target = results.getClosestCollision().getGeometry(); +// for (ArmatureDebugger skeleton : armatures.values()) { +// Joint selectedBone = skeleton.select(target); +// if (selectedBone != null) { +// selectedBones.put(skeleton.getArmature(), selectedBone); +// System.err.println("-----------------------"); +// System.err.println("Selected Bone : " + selectedBone.getName() + " in skeleton " + skeleton.getName()); +// System.err.println("Root Bone : " + (selectedBone.getParent() == null)); +// System.err.println("-----------------------"); +// System.err.println("Bind translation: " + selectedBone.getBindPosition()); +// System.err.println("Bind rotation: " + selectedBone.getBindRotation()); +// System.err.println("Bind scale: " + selectedBone.getBindScale()); +// System.err.println("---"); +// System.err.println("Local translation: " + selectedBone.getLocalPosition()); +// System.err.println("Local rotation: " + selectedBone.getLocalRotation()); +// System.err.println("Local scale: " + selectedBone.getLocalScale()); +// System.err.println("---"); +// System.err.println("Model translation: " + selectedBone.getModelSpacePosition()); +// System.err.println("Model rotation: " + selectedBone.getModelSpaceRotation()); +// System.err.println("Model scale: " + selectedBone.getModelSpaceScale()); +// System.err.println("---"); +// System.err.println("Bind inverse Transform: "); +// System.err.println(selectedBone.getBindInverseTransform()); +// return; +// } +// } +// } +// } + } + }; + +// public Map getSelectedBones() { +// return selectedBones; +// } + + public Node getDebugNode() { + return debugNode; + } + + public void setDebugNode(Node debugNode) { + this.debugNode = debugNode; + } +} diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugger.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java similarity index 63% rename from jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugger.java rename to jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java index 1336b9d1b..7899cabac 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugger.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java @@ -32,46 +32,37 @@ package com.jme3.scene.debug.custom; * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import com.jme3.animation.Bone; - -import java.util.Map; - -import com.jme3.animation.Skeleton; +import com.jme3.animation.*; import com.jme3.asset.AssetManager; import com.jme3.material.Material; import com.jme3.math.ColorRGBA; -import com.jme3.scene.BatchNode; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.Spatial; -import com.jme3.scene.VertexBuffer; +import com.jme3.scene.*; import java.nio.FloatBuffer; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; /** * The class that creates a mesh to display how bones behave. If it is supplied * with the bones' lengths it will show exactly how the bones look like on the * scene. If not then only connections between each bone heads will be shown. */ -public class SkeletonDebugger extends BatchNode { +public class ArmatureDebugger extends BatchNode { /** * The lines of the bones or the wires between their heads. */ - private SkeletonBone bones; + private ArmatureBone bones; - private Skeleton skeleton; + private Armature armature; /** * The dotted lines between a bone's tail and the had of its children. Not * available if the length data was not provided. */ - private SkeletonInterBoneWire interBoneWires; - private List selectedBones = new ArrayList(); + private ArmatureInterJointsWire interJointWires; + private Geometry wires; + private List selectedJoints = new ArrayList(); - public SkeletonDebugger() { + public ArmatureDebugger() { } /** @@ -80,38 +71,42 @@ public class SkeletonDebugger extends BatchNode { * and no dotted line of inter bones connection will be visible. * * @param name the name of the debugger's node - * @param skeleton the skeleton that will be shown + * @param armature the armature that will be shown */ - public SkeletonDebugger(String name, Skeleton skeleton, boolean guessBonesOrientation) { + public ArmatureDebugger(String name, Armature armature, boolean guessJointsOrientation) { super(name); - this.skeleton = skeleton; - skeleton.reset(); - skeleton.updateWorldVectors(); - Map boneLengths = new HashMap(); - - for (Bone bone : skeleton.getRoots()) { - computeLength(bone, boneLengths, skeleton); + this.armature = armature; +// armature.reset(); + armature.update(); + //Joints have no length we want to display the as bones so we compute their length + Map bonesLength = new HashMap(); + + for (Joint joint : armature.getRoots()) { + computeLength(joint, bonesLength, armature); } - bones = new SkeletonBone(skeleton, boneLengths, guessBonesOrientation); + bones = new ArmatureBone(armature, bonesLength, guessJointsOrientation); this.attachChild(bones); - interBoneWires = new SkeletonInterBoneWire(skeleton, boneLengths, guessBonesOrientation); - Geometry g = new Geometry(name + "_interwires", interBoneWires); - g.setBatchHint(BatchHint.Never); - this.attachChild(g); + interJointWires = new ArmatureInterJointsWire(armature, bonesLength, guessJointsOrientation); + wires = new Geometry(name + "_interwires", interJointWires); + this.attachChild(wires); } protected void initialize(AssetManager assetManager) { - Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", new ColorRGBA(0.05f, 0.05f, 0.05f, 1.0f));//new ColorRGBA(0.1f, 0.1f, 0.1f, 1.0f) + Material mat = new Material(assetManager, "Common/MatDefs/Misc/fakeLighting.j3md"); + mat.setColor("Color", new ColorRGBA(0.2f, 0.2f, 0.2f, 1)); setMaterial(mat); - Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat2.setBoolean("VertexColor", true); - bones.setMaterial(mat2); - batch(); + Material matWires = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + matWires.setColor("Color", ColorRGBA.Black); + wires.setMaterial(matWires); + //wires.setQueueBucket(RenderQueue.Bucket.Transparent); +// Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); +// mat2.setBoolean("VertexColor", true); +// bones.setMaterial(mat2); +// batch(); } @Override @@ -125,29 +120,29 @@ public class SkeletonDebugger extends BatchNode { } } - public Skeleton getSkeleton() { - return skeleton; + public Armature getArmature() { + return armature; } - private void computeLength(Bone b, Map boneLengths, Skeleton skeleton) { - if (b.getChildren().isEmpty()) { - if (b.getParent() != null) { - boneLengths.put(skeleton.getBoneIndex(b), boneLengths.get(skeleton.getBoneIndex(b.getParent())) * 0.75f); + private void computeLength(Joint joint, Map jointsLength, Armature armature) { + if (joint.getChildren().isEmpty()) { + if (joint.getParent() != null) { + jointsLength.put(armature.getJointIndex(joint), jointsLength.get(armature.getJointIndex(joint.getParent())) * 0.75f); } else { - boneLengths.put(skeleton.getBoneIndex(b), 0.1f); + jointsLength.put(armature.getJointIndex(joint), 0.1f); } } else { float length = Float.MAX_VALUE; - for (Bone bone : b.getChildren()) { - float len = b.getModelSpacePosition().subtract(bone.getModelSpacePosition()).length(); + for (Joint child : joint.getChildren()) { + float len = joint.getModelTransform().getTranslation().subtract(child.getModelTransform().getTranslation()).length(); if (len < length) { length = len; } } - boneLengths.put(skeleton.getBoneIndex(b), length); - for (Bone bone : b.getChildren()) { - computeLength(bone, boneLengths, skeleton); + jointsLength.put(armature.getJointIndex(joint), length); + for (Joint child : joint.getChildren()) { + computeLength(child, jointsLength, armature); } } } @@ -156,17 +151,17 @@ public class SkeletonDebugger extends BatchNode { public void updateLogicalState(float tpf) { super.updateLogicalState(tpf); bones.updateGeometry(); - if (interBoneWires != null) { - interBoneWires.updateGeometry(); + if (interJointWires != null) { + interJointWires.updateGeometry(); } } ColorRGBA selectedColor = ColorRGBA.Orange; ColorRGBA baseColor = new ColorRGBA(0.05f, 0.05f, 0.05f, 1f); - protected Bone select(Geometry g) { + protected Joint select(Geometry g) { Node oldNode = bones.getSelectedNode(); - Bone b = bones.select(g); + Joint b = bones.select(g); if (b == null) { return null; } @@ -178,17 +173,17 @@ public class SkeletonDebugger extends BatchNode { } /** - * @return the skeleton wires + * @return the armature wires */ - public SkeletonBone getBoneShapes() { + public ArmatureBone getBoneShapes() { return bones; } /** * @return the dotted line between bones (can be null) */ - public SkeletonInterBoneWire getInterBoneWires() { - return interBoneWires; + public ArmatureInterJointsWire getInterJointWires() { + return interJointWires; } protected void markSelected(Node n, boolean selected) { diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonInterBoneWire.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java similarity index 70% rename from jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonInterBoneWire.java rename to jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java index 504c81fe2..fbd97c1e0 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonInterBoneWire.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java @@ -33,34 +33,32 @@ package com.jme3.scene.debug.custom; */ -import java.nio.FloatBuffer; -import java.util.Map; - -import com.jme3.animation.Bone; -import com.jme3.animation.Skeleton; +import com.jme3.animation.Armature; +import com.jme3.animation.Joint; import com.jme3.math.Vector3f; import com.jme3.scene.Mesh; import com.jme3.scene.VertexBuffer; -import com.jme3.scene.VertexBuffer.Format; -import com.jme3.scene.VertexBuffer.Type; -import com.jme3.scene.VertexBuffer.Usage; +import com.jme3.scene.VertexBuffer.*; import com.jme3.util.BufferUtils; +import java.nio.FloatBuffer; +import java.util.Map; + /** * A class that displays a dotted line between a bone tail and its childrens' heads. * * @author Marcin Roguski (Kaelthas) */ -public class SkeletonInterBoneWire extends Mesh { - private static final int POINT_AMOUNT = 10; +public class ArmatureInterJointsWire extends Mesh { + private static final int POINT_AMOUNT = 50; /** * The amount of connections between bones. */ private int connectionsAmount; /** - * The skeleton that will be showed. + * The armature that will be showed. */ - private Skeleton skeleton; + private Armature armature; /** * The map between the bone index and its length. */ @@ -71,18 +69,17 @@ public class SkeletonInterBoneWire extends Mesh { /** * Creates buffers for points. Each line has POINT_AMOUNT of points. * - * @param skeleton the skeleton that will be showed + * @param armature the armature that will be showed * @param boneLengths the lengths of the bones */ - public SkeletonInterBoneWire(Skeleton skeleton, Map boneLengths, boolean guessBonesOrientation) { - this.skeleton = skeleton; + public ArmatureInterJointsWire(Armature armature, Map boneLengths, boolean guessBonesOrientation) { + this.armature = armature; - for (Bone bone : skeleton.getRoots()) { - this.countConnections(bone); + for (Joint joint : armature.getRoots()) { + this.countConnections(joint); } this.setMode(Mode.Points); - this.setPointSize(2); this.boneLengths = boneLengths; VertexBuffer pb = new VertexBuffer(Type.Position); @@ -95,27 +92,28 @@ public class SkeletonInterBoneWire extends Mesh { } /** - * The method updates the geometry according to the poitions of the bones. + * The method updates the geometry according to the positions of the bones. */ public void updateGeometry() { VertexBuffer vb = this.getBuffer(Type.Position); FloatBuffer posBuf = this.getFloatBuffer(Type.Position); posBuf.clear(); - for (int i = 0; i < skeleton.getBoneCount(); ++i) { - Bone bone = skeleton.getBone(i); - Vector3f parentTail = bone.getModelSpacePosition().add(bone.getModelSpaceRotation().mult(Vector3f.UNIT_Y.mult(boneLengths.get(i)))); + for (int i = 0; i < armature.getJointCount(); ++i) { + Joint joint = armature.getJoint(i); + Vector3f parentTail = joint.getModelTransform().getTranslation().add(joint.getModelTransform().getRotation().mult(Vector3f.UNIT_Y.mult(boneLengths.get(i)))); if (guessBonesOrientation) { - parentTail = bone.getModelSpacePosition(); + parentTail = joint.getModelTransform().getTranslation(); } - for (Bone child : bone.getChildren()) { - Vector3f childHead = child.getModelSpacePosition(); + for (Joint child : joint.getChildren()) { + Vector3f childHead = child.getModelTransform().getTranslation(); Vector3f v = childHead.subtract(parentTail); - float pointDelta = v.length() / POINT_AMOUNT; + float len = v.length(); + float pointDelta = 1f / POINT_AMOUNT; v.normalizeLocal().multLocal(pointDelta); Vector3f pointPosition = parentTail.clone(); - for (int j = 0; j < POINT_AMOUNT; ++j) { + for (int j = 0; j < POINT_AMOUNT * len; ++j) { posBuf.put(pointPosition.getX()).put(pointPosition.getY()).put(pointPosition.getZ()); pointPosition.addLocal(v); } @@ -128,12 +126,12 @@ public class SkeletonInterBoneWire extends Mesh { } /** - * Th method couns the connections between bones. + * Th method counts the connections between bones. * - * @param bone the bone where counting starts + * @param joint the bone where counting starts */ - private void countConnections(Bone bone) { - for (Bone child : bone.getChildren()) { + private void countConnections(Joint joint) { + for (Joint child : joint.getChildren()) { ++connectionsAmount; this.countConnections(child); } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java index 1b6208619..702ec5613 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java @@ -32,20 +32,11 @@ // $Id: Cylinder.java 4131 2009-03-19 20:15:28Z blaine.dev $ package com.jme3.scene.debug.custom; -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.Vector3f; -import com.jme3.scene.Mesh; +import com.jme3.math.*; import com.jme3.scene.VertexBuffer.Type; -import com.jme3.scene.mesh.IndexBuffer; +import com.jme3.scene.shape.AbstractBox; import com.jme3.util.BufferUtils; -import static com.jme3.util.BufferUtils.*; - -import java.io.IOException; import java.nio.FloatBuffer; /** @@ -55,362 +46,125 @@ import java.nio.FloatBuffer; * @author Mark Powell * @version $Revision: 4131 $, $Date: 2009-03-19 16:15:28 -0400 (Thu, 19 Mar 2009) $ */ -public class BoneShape extends Mesh { - - private int axisSamples; - - private int radialSamples; - - private float radius; - private float radius2; - - private float height; - private boolean closed; - private boolean inverted; - - /** - * Default constructor for serialization only. Do not use. - */ - public BoneShape() { +public class BoneShape extends AbstractBox { + + private static Vector3f topN = new Vector3f(0, 1, 0); + private static Vector3f botN = new Vector3f(0, -1, 0); + private static Vector3f rigN = new Vector3f(1, 0, 0); + private static Vector3f lefN = new Vector3f(-1, 0, 0); + + static { + Quaternion q = new Quaternion().fromAngleAxis(-FastMath.PI / 16f, Vector3f.UNIT_X); + q.multLocal(topN); + q.inverseLocal(); + q.multLocal(botN); + q = new Quaternion().fromAngleAxis(FastMath.PI / 16f, Vector3f.UNIT_Y); + q.multLocal(rigN); + q.inverseLocal(); + q.multLocal(lefN); } - /** - * Creates a new Cylinder. By default its center is the origin. Usually, a - * higher sample number creates a better looking cylinder, but at the cost - * of more vertex information. - * - * @param axisSamples Number of triangle samples along the axis. - * @param radialSamples Number of triangle samples along the radial. - * @param radius The radius of the cylinder. - * @param height The cylinder's height. - */ - public BoneShape(int axisSamples, int radialSamples, - float radius, float height) { - this(axisSamples, radialSamples, radius, height, false); - } - - /** - * Creates a new Cylinder. By default its center is the origin. Usually, a - * higher sample number creates a better looking cylinder, but at the cost - * of more vertex information.
- * If the cylinder is closed the texture is split into axisSamples parts: - * top most and bottom most part is used for top and bottom of the cylinder, - * rest of the texture for the cylinder wall. The middle of the top is - * mapped to texture coordinates (0.5, 1), bottom to (0.5, 0). Thus you need - * a suited distorted texture. - * - * @param axisSamples Number of triangle samples along the axis. - * @param radialSamples Number of triangle samples along the radial. - * @param radius The radius of the cylinder. - * @param height The cylinder's height. - * @param closed true to create a cylinder with top and bottom surface - */ - public BoneShape(int axisSamples, int radialSamples, - float radius, float height, boolean closed) { - this(axisSamples, radialSamples, radius, height, closed, false); - } + private static final short[] GEOMETRY_INDICES_DATA = { + 2, 1, 0, 3, 2, 0, // back + 6, 5, 4, 7, 6, 4, // right + 10, 9, 8, 11, 10, 8, // front + 14, 13, 12, 15, 14, 12, // left + 18, 17, 16, 19, 18, 16, // top + 22, 21, 20, 23, 22, 20 // bottom + }; + + private static final float[] GEOMETRY_NORMALS_DATA = { + 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, // back + rigN.x, rigN.y, rigN.z, rigN.x, rigN.y, rigN.z, rigN.x, rigN.y, rigN.z, rigN.x, rigN.y, rigN.z, // right + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // front + lefN.x, lefN.y, lefN.z, lefN.x, lefN.y, lefN.z, lefN.x, lefN.y, lefN.z, lefN.x, lefN.y, lefN.z, // left + topN.x, topN.y, topN.z, topN.x, topN.y, topN.z, topN.x, topN.y, topN.z, topN.x, topN.y, topN.z, // top + botN.x, botN.y, botN.z, botN.x, botN.y, botN.z, botN.x, botN.y, botN.z, botN.x, botN.y, botN.z // bottom + }; + + private static final float[] GEOMETRY_TEXTURE_DATA = { + 1, 0, 0, 0, 0, 1, 1, 1, // back + 1, 0, 0, 0, 0, 1, 1, 1, // right + 1, 0, 0, 0, 0, 1, 1, 1, // front + 1, 0, 0, 0, 0, 1, 1, 1, // left + 1, 0, 0, 0, 0, 1, 1, 1, // top + 1, 0, 0, 0, 0, 1, 1, 1 // bottom + }; + + private static final float[] GEOMETRY_POSITION_DATA = { + -0.5f, -0.5f, 0, 0.5f, -0.5f, 0, 0.5f, 0.5f, 0, -0.5f, 0.5f, 0, //back + 0.5f, -0.5f, 0, 0.25f, -0.25f, 1, 0.25f, 0.25f, 1, 0.5f, 0.5f, 0, //right + 0.25f, -0.25f, 1, -0.25f, -0.25f, 1, -0.25f, 0.25f, 1, 0.25f, 0.25f, 1, //front + -0.25f, -0.25f, 1, -0.5f, -0.5f, 0, -0.5f, 0.5f, 0, -0.25f, 0.25f, 1, //left + 0.5f, 0.5f, 0, 0.25f, 0.25f, 1, -0.25f, 0.25f, 1, -0.5f, 0.5f, 0, // top + -0.5f, -0.5f, 0, -0.25f, -0.25f, 1, 0.25f, -0.25f, 1, 0.5f, -0.5f, 0 // bottom + }; + + //0,1,2,3 + //1,4,6,2 + //4,5,7,6 + //5,0,3,7, + //2,6,7,3 + //0,5,4,1 + + +// v[0].x, v[0].y, v[0].z, v[1].x, v[1].y, v[1].z, v[2].x, v[2].y, v[2].z, v[3].x, v[3].y, v[3].z, // back +// v[1].x, v[1].y, v[1].z, v[4].x, v[4].y, v[4].z, v[6].x, v[6].y, v[6].z, v[2].x, v[2].y, v[2].z, // right +// v[4].x, v[4].y, v[4].z, v[5].x, v[5].y, v[5].z, v[7].x, v[7].y, v[7].z, v[6].x, v[6].y, v[6].z, // front +// v[5].x, v[5].y, v[5].z, v[0].x, v[0].y, v[0].z, v[3].x, v[3].y, v[3].z, v[7].x, v[7].y, v[7].z, // left +// v[2].x, v[2].y, v[2].z, v[6].x, v[6].y, v[6].z, v[7].x, v[7].y, v[7].z, v[3].x, v[3].y, v[3].z, // top +// v[0].x, v[0].y, v[0].z, v[5].x, v[5].y, v[5].z, v[4].x, v[4].y, v[4].z, v[1].x, v[1].y, v[1].z // bottom /** - * Creates a new Cylinder. By default its center is the origin. Usually, a - * higher sample number creates a better looking cylinder, but at the cost - * of more vertex information.
- * If the cylinder is closed the texture is split into axisSamples parts: - * top most and bottom most part is used for top and bottom of the cylinder, - * rest of the texture for the cylinder wall. The middle of the top is - * mapped to texture coordinates (0.5, 1), bottom to (0.5, 0). Thus you need - * a suited distorted texture. + * Creates a new box. + *

+ * The box has a center of 0,0,0 and extends in the out from the center by + * the given amount in each direction. So, for example, a box + * with extent of 0.5 would be the unit cube. * - * @param axisSamples Number of triangle samples along the axis. - * @param radialSamples Number of triangle samples along the radial. - * @param radius The radius of the cylinder. - * @param height The cylinder's height. - * @param closed true to create a cylinder with top and bottom surface - * @param inverted true to create a cylinder that is meant to be viewed from the - * interior. + * @param x the size of the box along the x axis, in both directions. + * @param y the size of the box along the y axis, in both directions. + * @param z the size of the box along the z axis, in both directions. */ - public BoneShape(int axisSamples, int radialSamples, - float radius, float height, boolean closed, boolean inverted) { - this(axisSamples, radialSamples, radius, radius, height, closed, inverted); - } - - public BoneShape(int axisSamples, int radialSamples, - float radius, float radius2, float height, boolean closed, boolean inverted) { + public BoneShape() { super(); - updateGeometry(axisSamples, radialSamples, radius, radius2, height, closed, inverted); - } - - /** - * @return the number of samples along the cylinder axis - */ - public int getAxisSamples() { - return axisSamples; - } - - /** - * @return Returns the height. - */ - public float getHeight() { - return height; - } - - /** - * @return number of samples around cylinder - */ - public int getRadialSamples() { - return radialSamples; - } - - /** - * @return Returns the radius. - */ - public float getRadius() { - return radius; - } - - public float getRadius2() { - return radius2; - } - - /** - * @return true if end caps are used. - */ - public boolean isClosed() { - return closed; + updateGeometry(); } /** - * @return true if normals and uvs are created for interior use + * Creates a clone of this box. + *

+ * The cloned box will have '_clone' appended to it's name, but all other + * properties will be the same as this box. */ - public boolean isInverted() { - return inverted; + @Override + public BoneShape clone() { + return new BoneShape(); } - /** - * Rebuilds the cylinder based on a new set of parameters. - * - * @param axisSamples the number of samples along the axis. - * @param radialSamples the number of samples around the radial. - * @param radius the radius of the bottom of the cylinder. - * @param radius2 the radius of the top of the cylinder. - * @param height the cylinder's height. - * @param closed should the cylinder have top and bottom surfaces. - * @param inverted is the cylinder is meant to be viewed from the inside. - */ - public void updateGeometry(int axisSamples, int radialSamples, - float radius, float radius2, float height, boolean closed, boolean inverted) { - this.axisSamples = axisSamples + (closed ? 2 : 0); - this.radialSamples = radialSamples; - this.radius = radius; - this.radius2 = radius2; - this.height = height; - this.closed = closed; - this.inverted = inverted; - -// VertexBuffer pvb = getBuffer(Type.Position); -// VertexBuffer nvb = getBuffer(Type.Normal); -// VertexBuffer tvb = getBuffer(Type.TexCoord); - - // Vertices - int vertCount = axisSamples * (radialSamples + 1) + (closed ? 2 : 0); - - setBuffer(Type.Position, 3, createVector3Buffer(getFloatBuffer(Type.Position), vertCount)); - - // Normals - setBuffer(Type.Normal, 3, createVector3Buffer(getFloatBuffer(Type.Normal), vertCount)); - - // Texture co-ordinates - setBuffer(Type.TexCoord, 2, createVector2Buffer(vertCount)); - - int triCount = ((closed ? 2 : 0) + 2 * (axisSamples - 1)) * radialSamples; - - setBuffer(Type.Index, 3, createShortBuffer(getShortBuffer(Type.Index), 3 * triCount)); - - //Color - setBuffer(Type.Color, 4, createFloatBuffer(vertCount * 4)); - - // generate geometry - float inverseRadial = 1.0f / radialSamples; - float inverseAxisLess = 1.0f / (closed ? axisSamples - 3 : axisSamples - 1); - float inverseAxisLessTexture = 1.0f / (axisSamples - 1); - float halfHeight = 0.5f * height; - - // Generate points on the unit circle to be used in computing the mesh - // points on a cylinder slice. - float[] sin = new float[radialSamples + 1]; - float[] cos = new float[radialSamples + 1]; - - for (int radialCount = 0; radialCount < radialSamples; radialCount++) { - float angle = FastMath.TWO_PI * inverseRadial * radialCount; - cos[radialCount] = FastMath.cos(angle); - sin[radialCount] = FastMath.sin(angle); - } - sin[radialSamples] = sin[0]; - cos[radialSamples] = cos[0]; - - // calculate normals - Vector3f[] vNormals = null; - Vector3f vNormal = Vector3f.UNIT_Z; - - if ((height != 0.0f) && (radius != radius2)) { - vNormals = new Vector3f[radialSamples]; - Vector3f vHeight = Vector3f.UNIT_Z.mult(height); - Vector3f vRadial = new Vector3f(); - - for (int radialCount = 0; radialCount < radialSamples; radialCount++) { - vRadial.set(cos[radialCount], sin[radialCount], 0.0f); - Vector3f vRadius = vRadial.mult(radius); - Vector3f vRadius2 = vRadial.mult(radius2); - Vector3f vMantle = vHeight.subtract(vRadius2.subtract(vRadius)); - Vector3f vTangent = vRadial.cross(Vector3f.UNIT_Z); - vNormals[radialCount] = vMantle.cross(vTangent).normalize(); - } - } - - FloatBuffer nb = getFloatBuffer(Type.Normal); - FloatBuffer pb = getFloatBuffer(Type.Position); - FloatBuffer tb = getFloatBuffer(Type.TexCoord); - FloatBuffer cb = getFloatBuffer(Type.Color); - - cb.rewind(); - for (int i = 0; i < vertCount; i++) { - cb.put(0.05f).put(0.05f).put(0.05f).put(1f); - } - - // generate the cylinder itself - Vector3f tempNormal = new Vector3f(); - for (int axisCount = 0, i = 0; axisCount < axisSamples; axisCount++, i++) { - float axisFraction; - float axisFractionTexture; - int topBottom = 0; - if (!closed) { - axisFraction = axisCount * inverseAxisLess; // in [0,1] - axisFractionTexture = axisFraction; - } else { - if (axisCount == 0) { - topBottom = -1; // bottom - axisFraction = 0; - axisFractionTexture = inverseAxisLessTexture; - } else if (axisCount == axisSamples - 1) { - topBottom = 1; // top - axisFraction = 1; - axisFractionTexture = 1 - inverseAxisLessTexture; - } else { - axisFraction = (axisCount - 1) * inverseAxisLess; - axisFractionTexture = axisCount * inverseAxisLessTexture; - } - } - - // compute center of slice - float z = height * axisFraction; - Vector3f sliceCenter = new Vector3f(0, 0, z); - - // compute slice vertices with duplication at end point - int save = i; - for (int radialCount = 0; radialCount < radialSamples; radialCount++, i++) { - float radialFraction = radialCount * inverseRadial; // in [0,1) - tempNormal.set(cos[radialCount], sin[radialCount], 0.0f); - - if (vNormals != null) { - vNormal = vNormals[radialCount]; - } else if (radius == radius2) { - vNormal = tempNormal; - } - - if (topBottom == 0) { - if (!inverted) - nb.put(vNormal.x).put(vNormal.y).put(vNormal.z); - else - nb.put(-vNormal.x).put(-vNormal.y).put(-vNormal.z); - } else { - nb.put(0).put(0).put(topBottom * (inverted ? -1 : 1)); - } - - tempNormal.multLocal((radius - radius2) * axisFraction + radius2) - .addLocal(sliceCenter); - pb.put(tempNormal.x).put(tempNormal.y).put(tempNormal.z); - - tb.put((inverted ? 1 - radialFraction : radialFraction)) - .put(axisFractionTexture); - } - - BufferUtils.copyInternalVector3(pb, save, i); - BufferUtils.copyInternalVector3(nb, save, i); - - tb.put((inverted ? 0.0f : 1.0f)) - .put(axisFractionTexture); - } - - if (closed) { - pb.put(0).put(0).put(-halfHeight); // bottom center - nb.put(0).put(0).put(-1 * (inverted ? -1 : 1)); - tb.put(0.5f).put(0); - pb.put(0).put(0).put(halfHeight); // top center - nb.put(0).put(0).put(1 * (inverted ? -1 : 1)); - tb.put(0.5f).put(1); + protected void doUpdateGeometryIndices() { + if (getBuffer(Type.Index) == null) { + setBuffer(Type.Index, 3, BufferUtils.createShortBuffer(GEOMETRY_INDICES_DATA)); } + } - IndexBuffer ib = getIndexBuffer(); - int index = 0; - // Connectivity - for (int axisCount = 0, axisStart = 0; axisCount < axisSamples - 1; axisCount++) { - int i0 = axisStart; - int i1 = i0 + 1; - axisStart += radialSamples + 1; - int i2 = axisStart; - int i3 = i2 + 1; - for (int i = 0; i < radialSamples; i++) { - if (closed && axisCount == 0) { - if (!inverted) { - ib.put(index++, i0++); - ib.put(index++, vertCount - 2); - ib.put(index++, i1++); - } else { - ib.put(index++, i0++); - ib.put(index++, i1++); - ib.put(index++, vertCount - 2); - } - } else if (closed && axisCount == axisSamples - 2) { - ib.put(index++, i2++); - ib.put(index++, inverted ? vertCount - 1 : i3++); - ib.put(index++, inverted ? i3++ : vertCount - 1); - } else { - ib.put(index++, i0++); - ib.put(index++, inverted ? i2 : i1); - ib.put(index++, inverted ? i1 : i2); - ib.put(index++, i1++); - ib.put(index++, inverted ? i2++ : i3++); - ib.put(index++, inverted ? i3++ : i2++); - } - } + protected void doUpdateGeometryNormals() { + if (getBuffer(Type.Normal) == null) { + setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(GEOMETRY_NORMALS_DATA)); } - - updateBound(); } - @Override - public void read(JmeImporter e) throws IOException { - super.read(e); - InputCapsule capsule = e.getCapsule(this); - axisSamples = capsule.readInt("axisSamples", 0); - radialSamples = capsule.readInt("radialSamples", 0); - radius = capsule.readFloat("radius", 0); - radius2 = capsule.readFloat("radius2", 0); - height = capsule.readFloat("height", 0); - closed = capsule.readBoolean("closed", false); - inverted = capsule.readBoolean("inverted", false); + protected void doUpdateGeometryTextures() { + if (getBuffer(Type.TexCoord) == null) { + setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(GEOMETRY_TEXTURE_DATA)); + } } - @Override - public void write(JmeExporter e) throws IOException { - super.write(e); - OutputCapsule capsule = e.getCapsule(this); - capsule.write(axisSamples, "axisSamples", 0); - capsule.write(radialSamples, "radialSamples", 0); - capsule.write(radius, "radius", 0); - capsule.write(radius2, "radius2", 0); - capsule.write(height, "height", 0); - capsule.write(closed, "closed", false); - capsule.write(inverted, "inverted", false); + protected void doUpdateGeometryVertices() { + FloatBuffer fpb = BufferUtils.createVector3Buffer(24); + fpb.put(GEOMETRY_POSITION_DATA); + setBuffer(Type.Position, 3, fpb); + updateBound(); } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java deleted file mode 100644 index 43778f5ce..000000000 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * To change this template, choose Tools | Templates - * and open the template in the editor. - */ -package com.jme3.scene.debug.custom; - -import com.jme3.animation.Bone; -import com.jme3.animation.Skeleton; -import com.jme3.animation.SkeletonControl; -import com.jme3.app.Application; -import com.jme3.app.state.AbstractAppState; -import com.jme3.app.state.AppStateManager; -import com.jme3.collision.CollisionResults; -import com.jme3.input.MouseInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.MouseButtonTrigger; -import com.jme3.math.Ray; -import com.jme3.math.Vector2f; -import com.jme3.math.Vector3f; -import com.jme3.renderer.ViewPort; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.Spatial; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * @author Nehon - */ -public class SkeletonDebugAppState extends AbstractAppState { - - private Node debugNode = new Node("debugNode"); - private Map skeletons = new HashMap(); - private Map selectedBones = new HashMap(); - private Application app; - - @Override - public void initialize(AppStateManager stateManager, Application app) { - ViewPort vp = app.getRenderManager().createMainView("debug", app.getCamera()); - vp.attachScene(debugNode); - vp.setClearDepth(true); - this.app = app; - for (SkeletonDebugger skeletonDebugger : skeletons.values()) { - skeletonDebugger.initialize(app.getAssetManager()); - } - app.getInputManager().addListener(actionListener, "shoot"); - app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); - super.initialize(stateManager, app); - } - - @Override - public void update(float tpf) { - debugNode.updateLogicalState(tpf); - debugNode.updateGeometricState(); - } - - public SkeletonDebugger addSkeleton(SkeletonControl skeletonControl, boolean guessBonesOrientation) { - Skeleton skeleton = skeletonControl.getSkeleton(); - Spatial forSpatial = skeletonControl.getSpatial(); - return addSkeleton(skeleton, forSpatial, guessBonesOrientation); - } - - public SkeletonDebugger addSkeleton(Skeleton skeleton, Spatial forSpatial, boolean guessBonesOrientation) { - - SkeletonDebugger sd = new SkeletonDebugger(forSpatial.getName() + "_Skeleton", skeleton, guessBonesOrientation); - sd.setLocalTransform(forSpatial.getWorldTransform()); - if (forSpatial instanceof Node) { - List geoms = new ArrayList<>(); - findGeoms((Node) forSpatial, geoms); - if (geoms.size() == 1) { - sd.setLocalTransform(geoms.get(0).getWorldTransform()); - } - } - skeletons.put(skeleton, sd); - debugNode.attachChild(sd); - if (isInitialized()) { - sd.initialize(app.getAssetManager()); - } - return sd; - } - - private void findGeoms(Node node, List geoms) { - for (Spatial spatial : node.getChildren()) { - if (spatial instanceof Geometry) { - geoms.add((Geometry) spatial); - } else if (spatial instanceof Node) { - findGeoms((Node) spatial, geoms); - } - } - } - - /** - * Pick a Target Using the Mouse Pointer.

  1. Map "pick target" action - * to a MouseButtonTrigger.
  2. flyCam.setEnabled(false); - *
  3. inputManager.setCursorVisible(true);
  4. Implement action in - * AnalogListener (TODO).
- */ - private ActionListener actionListener = new ActionListener() { - public void onAction(String name, boolean isPressed, float tpf) { - if (name.equals("shoot") && isPressed) { - CollisionResults results = new CollisionResults(); - Vector2f click2d = app.getInputManager().getCursorPosition(); - Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); - Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); - Ray ray = new Ray(click3d, dir); - - debugNode.collideWith(ray, results); - - if (results.size() > 0) { - // The closest result is the target that the player picked: - Geometry target = results.getClosestCollision().getGeometry(); - for (SkeletonDebugger skeleton : skeletons.values()) { - Bone selectedBone = skeleton.select(target); - if (selectedBone != null) { - selectedBones.put(skeleton.getSkeleton(), selectedBone); - System.err.println("-----------------------"); - System.err.println("Selected Bone : " + selectedBone.getName() + " in skeleton " + skeleton.getName()); - System.err.println("Root Bone : " + (selectedBone.getParent() == null)); - System.err.println("-----------------------"); - System.err.println("Bind translation: " + selectedBone.getBindPosition()); - System.err.println("Bind rotation: " + selectedBone.getBindRotation()); - System.err.println("Bind scale: " + selectedBone.getBindScale()); - System.err.println("---"); - System.err.println("Local translation: " + selectedBone.getLocalPosition()); - System.err.println("Local rotation: " + selectedBone.getLocalRotation()); - System.err.println("Local scale: " + selectedBone.getLocalScale()); - System.err.println("---"); - System.err.println("Model translation: " + selectedBone.getModelSpacePosition()); - System.err.println("Model rotation: " + selectedBone.getModelSpaceRotation()); - System.err.println("Model scale: " + selectedBone.getModelSpaceScale()); - System.err.println("---"); - System.err.println("Bind inverse Transform: "); - System.err.println(selectedBone.getBindInverseTransform()); - return; - } - } - } - } - } - }; - - public Map getSelectedBones() { - return selectedBones; - } - - public Node getDebugNode() { - return debugNode; - } - - public void setDebugNode(Node debugNode) { - this.debugNode = debugNode; - } -} diff --git a/jme3-core/src/main/java/com/jme3/scene/shape/AbstractBox.java b/jme3-core/src/main/java/com/jme3/scene/shape/AbstractBox.java index 7ffee1905..1571823a7 100644 --- a/jme3-core/src/main/java/com/jme3/scene/shape/AbstractBox.java +++ b/jme3-core/src/main/java/com/jme3/scene/shape/AbstractBox.java @@ -31,12 +31,10 @@ */ package com.jme3.scene.shape; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; +import com.jme3.export.*; import com.jme3.math.Vector3f; import com.jme3.scene.Mesh; + import java.io.IOException; /** @@ -88,12 +86,12 @@ public abstract class AbstractBox extends Mesh { /** * Convert the indices into the list of vertices that define the box's geometry. */ - protected abstract void duUpdateGeometryIndices(); + protected abstract void doUpdateGeometryIndices(); /** * Update the normals of each of the box's planes. */ - protected abstract void duUpdateGeometryNormals(); + protected abstract void doUpdateGeometryNormals(); /** * Update the points that define the texture of the box. @@ -101,14 +99,14 @@ public abstract class AbstractBox extends Mesh { * It's a one-to-one ratio, where each plane of the box has its own copy * of the texture. That is, the texture is repeated one time for each face. */ - protected abstract void duUpdateGeometryTextures(); + protected abstract void doUpdateGeometryTextures(); /** * Update the position of the vertices that define the box. *

* These eight points are determined from the minimum and maximum point. */ - protected abstract void duUpdateGeometryVertices(); + protected abstract void doUpdateGeometryVertices(); /** * Get the center point of this box. @@ -145,10 +143,10 @@ public abstract class AbstractBox extends Mesh { * need to call this method afterwards in order to update the box. */ public final void updateGeometry() { - duUpdateGeometryVertices(); - duUpdateGeometryNormals(); - duUpdateGeometryTextures(); - duUpdateGeometryIndices(); + doUpdateGeometryVertices(); + doUpdateGeometryNormals(); + doUpdateGeometryTextures(); + doUpdateGeometryIndices(); setStatic(); } diff --git a/jme3-core/src/main/java/com/jme3/scene/shape/Box.java b/jme3-core/src/main/java/com/jme3/scene/shape/Box.java index f94611de3..15b25d24b 100644 --- a/jme3-core/src/main/java/com/jme3/scene/shape/Box.java +++ b/jme3-core/src/main/java/com/jme3/scene/shape/Box.java @@ -35,6 +35,7 @@ package com.jme3.scene.shape; import com.jme3.math.Vector3f; import com.jme3.scene.VertexBuffer.Type; import com.jme3.util.BufferUtils; + import java.nio.FloatBuffer; /** @@ -144,25 +145,25 @@ public class Box extends AbstractBox { return new Box(center.clone(), xExtent, yExtent, zExtent); } - protected void duUpdateGeometryIndices() { + protected void doUpdateGeometryIndices() { if (getBuffer(Type.Index) == null){ setBuffer(Type.Index, 3, BufferUtils.createShortBuffer(GEOMETRY_INDICES_DATA)); } } - protected void duUpdateGeometryNormals() { + protected void doUpdateGeometryNormals() { if (getBuffer(Type.Normal) == null){ setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(GEOMETRY_NORMALS_DATA)); } } - protected void duUpdateGeometryTextures() { + protected void doUpdateGeometryTextures() { if (getBuffer(Type.TexCoord) == null){ setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(GEOMETRY_TEXTURE_DATA)); } } - protected void duUpdateGeometryVertices() { + protected void doUpdateGeometryVertices() { FloatBuffer fpb = BufferUtils.createVector3Buffer(24); Vector3f[] v = computeVertices(); fpb.put(new float[] { diff --git a/jme3-core/src/main/java/com/jme3/scene/shape/StripBox.java b/jme3-core/src/main/java/com/jme3/scene/shape/StripBox.java index 312650caa..5ac256e93 100644 --- a/jme3-core/src/main/java/com/jme3/scene/shape/StripBox.java +++ b/jme3-core/src/main/java/com/jme3/scene/shape/StripBox.java @@ -35,6 +35,7 @@ package com.jme3.scene.shape; import com.jme3.math.Vector3f; import com.jme3.scene.VertexBuffer.Type; import com.jme3.util.BufferUtils; + import java.nio.FloatBuffer; /** @@ -138,13 +139,13 @@ public class StripBox extends AbstractBox { return new StripBox(center.clone(), xExtent, yExtent, zExtent); } - protected void duUpdateGeometryIndices() { + protected void doUpdateGeometryIndices() { if (getBuffer(Type.Index) == null){ setBuffer(Type.Index, 3, BufferUtils.createShortBuffer(GEOMETRY_INDICES_DATA)); } } - protected void duUpdateGeometryNormals() { + protected void doUpdateGeometryNormals() { if (getBuffer(Type.Normal) == null){ float[] normals = new float[8 * 3]; @@ -163,13 +164,13 @@ public class StripBox extends AbstractBox { } } - protected void duUpdateGeometryTextures() { + protected void doUpdateGeometryTextures() { if (getBuffer(Type.TexCoord) == null){ setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(GEOMETRY_TEXTURE_DATA)); } } - protected void duUpdateGeometryVertices() { + protected void doUpdateGeometryVertices() { FloatBuffer fpb = BufferUtils.createVector3Buffer(8 * 3); Vector3f[] v = computeVertices(); fpb.put(new float[] { diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/fakeLighting.j3md b/jme3-core/src/main/resources/Common/MatDefs/Misc/fakeLighting.j3md new file mode 100644 index 000000000..58797c112 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/fakeLighting.j3md @@ -0,0 +1,49 @@ +MaterialDef FakeLighting { + MaterialParameters { + Vector4 Color + } + + Technique { + WorldParameters { + WorldViewProjectionMatrix + NormalMatrix + } + + VertexShaderNodes { + ShaderNode Mat3Vec3Mult { + Definition: Mat3Vec3Mult: Common/MatDefs/ShaderNodes/Basic/Mat3Vec3Mult.j3sn + InputMappings { + matrix3 = WorldParam.NormalMatrix + vector3 = Attr.inNormal + } + OutputMappings { + } + } + ShaderNode CommonVert { + Definition: CommonVert: Common/MatDefs/ShaderNodes/Common/CommonVert.j3sn + InputMappings { + worldViewProjectionMatrix = WorldParam.WorldViewProjectionMatrix + modelPosition = Attr.inPosition + } + OutputMappings { + Global.position = projPosition + } + } + } + + + FragmentShaderNodes { + ShaderNode FakeLighting { + Definition: FakeLighting: Common/MatDefs/ShaderNodes/Misc/fakeLighting.j3sn + InputMappings { + inColor = MatParam.Color + normal = Mat3Vec3Mult.outVector3.xyz + } + OutputMappings { + Global.color = outColor + } + } + } + + } +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Basic/Mat3Vec3Mult.j3sn b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Basic/Mat3Vec3Mult.j3sn new file mode 100644 index 000000000..019edb839 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Basic/Mat3Vec3Mult.j3sn @@ -0,0 +1,31 @@ +ShaderNodeDefinitions{ + ShaderNodeDefinition Mat3Vec3Mult { + //Vertex/Fragment + Type: Vertex + + //Shader GLSL: + Shader GLSL100: Common/MatDefs/ShaderNodes/Basic/Mat3Vec3Mult100.frag + + Documentation{ + //type documentation here. This is optional but recommended + + //@input + @input mat3 matrix3 the mat3 + @input vec3 vector3 the vec3 + + //@output + @output vec3 outVector3 the output vector + } + Input { + //all the node inputs + // + mat3 matrix3 + vec3 vector3 + } + Output { + //all the node outputs + // + vec3 outVector3 + } + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Basic/Mat3Vec3Mult100.frag b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Basic/Mat3Vec3Mult100.frag new file mode 100644 index 000000000..f1a29fd2c --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Basic/Mat3Vec3Mult100.frag @@ -0,0 +1,3 @@ +void main(){ + outVector3 = matrix3 * vector3; +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/fakeLighting.j3sn b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/fakeLighting.j3sn new file mode 100644 index 000000000..c47e1bf61 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/fakeLighting.j3sn @@ -0,0 +1,33 @@ +ShaderNodeDefinitions{ + ShaderNodeDefinition FakeLighting { + //Vertex/Fragment + Type: Fragment + + //Shader GLSL: + Shader GLSL100: Common/MatDefs/ShaderNodes/Misc/fakeLighting100.frag + + Documentation{ + //type documentation here. This is optional but recommended + + //@input + @input vec4 inColor The input color + @input vec3 normal The normal in view space + + + //@output + @output vec4 outColor The modified output color + } + Input { + //all the node inputs + // + vec4 inColor + vec3 normal + + } + Output { + //all the node outputs + // + vec4 outColor + } + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/fakeLighting100.frag b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/fakeLighting100.frag new file mode 100644 index 000000000..9b6b98b09 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/fakeLighting100.frag @@ -0,0 +1,9 @@ +void main(){ + + vec4 dark = inColor * 0.3; + vec4 bright = min(inColor * 4.0, 1.0); + normal = normalize(normal); + vec3 dir = vec3(0,0,1); + float factor = dot(dir, normal); + outColor = mix(dark, bright, factor); +} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java new file mode 100644 index 000000000..c4f23e882 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java @@ -0,0 +1,112 @@ +package jme3test.model.anim; + +import com.jme3.animation.*; +import com.jme3.app.ChaseCameraAppState; +import com.jme3.app.SimpleApplication; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.math.*; +import com.jme3.scene.*; +import com.jme3.scene.debug.custom.ArmatureDebugAppState; +import com.jme3.util.TangentBinormalGenerator; + +/** + * Created by Nehon on 18/12/2017. + */ +public class TestArmature extends SimpleApplication { + + Joint j1; + Joint j2; + + public static void main(String... argv) { + TestArmature app = new TestArmature(); + app.start(); + } + + @Override + public void simpleInitApp() { + renderManager.setSinglePassLightBatchSize(2); + //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + + Joint root = new Joint("Root_Joint"); + j1 = new Joint("Joint_1"); + j2 = new Joint("Joint_2"); + root.addChild(j1); + j1.addChild(j2); + j1.setLocalTranslation(new Vector3f(0, 0.5f, 0)); + j1.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * 0.3f, Vector3f.UNIT_Z)); + j2.setLocalTranslation(new Vector3f(0, 0.2f, 0)); + Joint[] joints = new Joint[]{root, j1, j2}; + + Armature armature = new Armature(joints); + armature.setBindPose(); + + ArmatureControl ac = new ArmatureControl(armature); + Node node = new Node("Test Armature"); + rootNode.attachChild(node); + + node.addControl(ac); + + ArmatureDebugAppState debugAppState = new ArmatureDebugAppState(); + debugAppState.addArmature(ac, true); + stateManager.attach(debugAppState); + + rootNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal())); + + rootNode.addLight(new DirectionalLight(new Vector3f(1f, 1f, 1f).normalizeLocal(), new ColorRGBA(0.7f, 0.7f, 0.7f, 1.0f))); + + + flyCam.setEnabled(false); + + ChaseCameraAppState chaseCam = new ChaseCameraAppState(); + chaseCam.setTarget(node); + getStateManager().attach(chaseCam); + chaseCam.setInvertHorizontalAxis(true); + chaseCam.setInvertVerticalAxis(true); + chaseCam.setZoomSpeed(0.5f); + chaseCam.setMinVerticalRotation(-FastMath.HALF_PI); + chaseCam.setRotationSpeed(3); + chaseCam.setDefaultDistance(3); + chaseCam.setMinDistance(0.01f); + chaseCam.setZoomSpeed(0.01f); + chaseCam.setDefaultVerticalRotation(0.3f); + } + + + private void displayNormals(Spatial s) { + final Node debugTangents = new Node("debug tangents"); + debugTangents.setCullHint(Spatial.CullHint.Never); + + rootNode.attachChild(debugTangents); + + final Material debugMat = assetManager.loadMaterial("Common/Materials/VertexColor.j3m"); + debugMat.getAdditionalRenderState().setLineWidth(2); + + s.depthFirstTraversal(new SceneGraphVisitorAdapter() { + @Override + public void visit(Geometry g) { + Mesh m = g.getMesh(); + Geometry debug = new Geometry( + "debug tangents geom", + TangentBinormalGenerator.genNormalLines(m, 0.1f) + ); + debug.setMaterial(debugMat); + debug.setCullHint(Spatial.CullHint.Never); + debug.setLocalTransform(g.getWorldTransform()); + debugTangents.attachChild(debug); + } + }); + } + + float time = 0; + + @Override + public void simpleUpdate(float tpf) { + time += tpf; + float rot = FastMath.sin(time); + j1.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * rot, Vector3f.UNIT_Z)); + j2.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * rot, Vector3f.UNIT_Z)); + + } +} From c3cb4ef97f6c8a8815da57bb9c993cc8f7edfcb6 Mon Sep 17 00:00:00 2001 From: Nehon Date: Wed, 20 Dec 2017 23:25:50 +0100 Subject: [PATCH 06/54] Draft of the new animation system --- .gitignore | 2 +- .../src/main/java/com/jme3/anim/AnimClip.java | 113 ++++++ .../main/java/com/jme3/anim/AnimComposer.java | 83 +++++ .../jme3/{animation => anim}/Armature.java | 49 +-- .../com/jme3/{animation => anim}/Joint.java | 42 ++- .../main/java/com/jme3/anim/JointTrack.java | 113 ++++++ .../SkinningControl.java} | 30 +- .../java/com/jme3/anim/TransformTrack.java | 344 ++++++++++++++++++ .../anim/interpolator/AnimInterpolator.java | 13 + .../anim/interpolator/AnimInterpolators.java | 151 ++++++++ .../anim/interpolator/FrameInterpolator.java | 121 ++++++ .../com/jme3/anim/tween/AbstractTween.java | 110 ++++++ .../main/java/com/jme3/anim/tween/Tween.java | 71 ++++ .../java/com/jme3/animation/AnimChannel.java | 2 + .../java/com/jme3/animation/AnimControl.java | 2 + .../com/jme3/animation/AnimEventListener.java | 1 + .../java/com/jme3/animation/Animation.java | 3 + .../java/com/jme3/animation/AudioTrack.java | 8 +- .../main/java/com/jme3/animation/Bone.java | 2 + .../java/com/jme3/animation/BoneTrack.java | 6 + .../com/jme3/animation/ClonableTrack.java | 1 + .../java/com/jme3/animation/EffectTrack.java | 15 +- .../java/com/jme3/animation/LoopMode.java | 1 + .../main/java/com/jme3/animation/Pose.java | 2 + .../java/com/jme3/animation/Skeleton.java | 6 +- .../com/jme3/animation/SkeletonControl.java | 3 + .../java/com/jme3/animation/SpatialTrack.java | 8 +- .../main/java/com/jme3/animation/Track.java | 1 + .../java/com/jme3/animation/TrackInfo.java | 8 +- .../main/java/com/jme3/math/EaseFunction.java | 13 + .../src/main/java/com/jme3/math/Easing.java | 163 +++++++++ .../main/java/com/jme3/math/MathUtils.java | 165 +++++++++ .../main/java/com/jme3/math/Transform.java | 3 +- .../jme3/scene/debug/custom/ArmatureBone.java | 4 +- .../debug/custom/ArmatureDebugAppState.java | 8 +- .../scene/debug/custom/ArmatureDebugger.java | 8 +- .../debug/custom/ArmatureInterJointsWire.java | 4 +- .../java/jme3test/model/TestGltfLoading.java | 15 +- .../java/jme3test/model/TestGltfLoading2.java | 13 +- .../java/jme3test/model/anim/EraseTimer.java | 76 ++++ .../jme3test/model/anim/TestArmature.java | 142 +++++++- 41 files changed, 1805 insertions(+), 120 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/anim/AnimClip.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/AnimComposer.java rename jme3-core/src/main/java/com/jme3/{animation => anim}/Armature.java (82%) rename jme3-core/src/main/java/com/jme3/{animation => anim}/Joint.java (92%) create mode 100644 jme3-core/src/main/java/com/jme3/anim/JointTrack.java rename jme3-core/src/main/java/com/jme3/{animation/ArmatureControl.java => anim/SkinningControl.java} (97%) create mode 100644 jme3-core/src/main/java/com/jme3/anim/TransformTrack.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolator.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolators.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/interpolator/FrameInterpolator.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/Tween.java create mode 100644 jme3-core/src/main/java/com/jme3/math/EaseFunction.java create mode 100644 jme3-core/src/main/java/com/jme3/math/Easing.java create mode 100644 jme3-core/src/main/java/com/jme3/math/MathUtils.java create mode 100644 jme3-examples/src/main/java/jme3test/model/anim/EraseTimer.java diff --git a/.gitignore b/.gitignore index ce4b6840a..065ad2311 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ **/.classpath **/.settings **/.project -**/out +**/out/ /.gradle/ /.nb-gradle/ /.idea/ diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java new file mode 100644 index 000000000..00eff2c05 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java @@ -0,0 +1,113 @@ +package com.jme3.anim; + +import com.jme3.anim.tween.Tween; +import com.jme3.export.*; +import com.jme3.util.SafeArrayList; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; + +/** + * Created by Nehon on 20/12/2017. + */ +public class AnimClip implements Tween, JmeCloneable, Savable { + + private String name; + private double length; + + private SafeArrayList tracks = new SafeArrayList<>(Tween.class); + + public AnimClip() { + } + + public AnimClip(String name) { + this.name = name; + } + + public void setTracks(Tween[] tracks) { + for (Tween track : tracks) { + addTrack(track); + } + } + + public void addTrack(Tween track) { + tracks.add(track); + if (track.getLength() > length) { + length = track.getLength(); + } + } + + public void removeTrack(Tween track) { + if (tracks.remove(track)) { + length = 0; + for (Tween t : tracks.getArray()) { + if (t.getLength() > length) { + length = t.getLength(); + } + } + } + } + + public String getName() { + return name; + } + + @Override + public double getLength() { + return length; + } + + @Override + public boolean interpolate(double t) { + // Sanity check the inputs + if (t < 0) { + return true; + } + + for (Tween track : tracks.getArray()) { + track.interpolate(t); + } + return t <= length; + } + + @Override + public Object jmeClone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException("Error cloning", e); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + SafeArrayList newTracks = new SafeArrayList<>(Tween.class); + for (Tween track : tracks) { + newTracks.add(cloner.clone(track)); + } + this.tracks = newTracks; + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule oc = ex.getCapsule(this); + oc.write(name, "name", null); + oc.write(tracks.getArray(), "tracks", null); + + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + name = ic.readString("name", null); + Savable[] arr = ic.readSavableArray("tracks", null); + if (arr != null) { + tracks = new SafeArrayList<>(Tween.class); + for (Savable savable : arr) { + tracks.add((Tween) savable); + } + } + } + +} diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java new file mode 100644 index 000000000..6a74a0c2c --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -0,0 +1,83 @@ +package com.jme3.anim; + +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.control.AbstractControl; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by Nehon on 20/12/2017. + */ +public class AnimComposer extends AbstractControl { + + private Map animClipMap = new HashMap<>(); + + private AnimClip currentAnimClip; + private float time; + + /** + * Retrieve an animation from the list of animations. + * + * @param name The name of the animation to retrieve. + * @return The animation corresponding to the given name, or null, if no + * such named animation exists. + */ + public AnimClip getAnimClip(String name) { + return animClipMap.get(name); + } + + /** + * Adds an animation to be available for playing to this + * AnimControl. + * + * @param anim The animation to add. + */ + public void addAnimClip(AnimClip anim) { + animClipMap.put(anim.getName(), anim); + } + + /** + * Remove an animation so that it is no longer available for playing. + * + * @param anim The animation to remove. + */ + public void removeAnimClip(AnimClip anim) { + if (!animClipMap.containsKey(anim.getName())) { + throw new IllegalArgumentException("Given animation does not exist " + + "in this AnimControl"); + } + + animClipMap.remove(anim.getName()); + } + + public void setCurrentAnimClip(String name) { + currentAnimClip = animClipMap.get(name); + time = 0; + if (currentAnimClip == null) { + throw new IllegalArgumentException("Unknown clip " + name); + } + } + + public void reset() { + currentAnimClip = null; + time = 0; + } + + @Override + protected void controlUpdate(float tpf) { + if (currentAnimClip != null) { + time += tpf; + boolean running = currentAnimClip.interpolate(time); + if (!running) { + time -= currentAnimClip.getLength(); + } + } + } + + @Override + protected void controlRender(RenderManager rm, ViewPort vp) { + + } +} diff --git a/jme3-core/src/main/java/com/jme3/animation/Armature.java b/jme3-core/src/main/java/com/jme3/anim/Armature.java similarity index 82% rename from jme3-core/src/main/java/com/jme3/animation/Armature.java rename to jme3-core/src/main/java/com/jme3/anim/Armature.java index 9c09e63a0..982e08d36 100644 --- a/jme3-core/src/main/java/com/jme3/animation/Armature.java +++ b/jme3-core/src/main/java/com/jme3/anim/Armature.java @@ -1,8 +1,7 @@ -package com.jme3.animation; +package com.jme3.anim; import com.jme3.export.*; import com.jme3.math.Matrix4f; -import com.jme3.util.TempVars; import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; @@ -60,33 +59,6 @@ public class Armature implements JmeCloneable, Savable { } } -// -// /** -// * Special-purpose copy constructor. -// *

-// * Shallow copies bind pose data from the source skeleton, does not -// * copy any other data. -// * -// * @param source The source Skeleton to copy from -// */ -// public Armature(Armature source) { -// Joint[] sourceList = source.jointList; -// jointList = new Joint[sourceList.length]; -// for (int i = 0; i < sourceList.length; i++) { -// jointList[i] = new Joint(sourceList[i]); -// } -// -// rootJoints = new Bone[source.rootJoints.length]; -// for (int i = 0; i < rootJoints.length; i++) { -// rootJoints[i] = recreateBoneStructure(source.rootJoints[i]); -// } -// createSkinningMatrices(); -// -// for (int i = rootJoints.length - 1; i >= 0; i--) { -// rootJoints[i].update(); -// } -// } - /** * Update all joints sin this Amature. */ @@ -176,8 +148,18 @@ public class Armature implements JmeCloneable, Savable { //make sure all bones are updated update(); //Save the current pose as bind pose - for (Joint rootJoint : rootJoints) { - rootJoint.setBindPose(); + for (Joint joint : jointList) { + joint.setBindPose(); + } + } + + /** + * This methods sets this armature in its bind pose (aligned with the undeformed mesh) + * Note that this is only useful for debugging porpose. + */ + public void resetToBindPose() { + for (Joint joint : rootJoints) { + joint.resetToBindPose(); } } @@ -187,11 +169,9 @@ public class Armature implements JmeCloneable, Savable { * @return */ public Matrix4f[] computeSkinningMatrices() { - TempVars vars = TempVars.get(); for (int i = 0; i < jointList.length; i++) { - jointList[i].getOffsetTransform(skinningMatrixes[i], vars.quat1, vars.vect1, vars.vect2, vars.tempMat3); + jointList[i].getOffsetTransform(skinningMatrixes[i]); } - vars.release(); return skinningMatrixes; } @@ -247,5 +227,4 @@ public class Armature implements JmeCloneable, Savable { output.write(rootJoints, "rootJoints", null); output.write(jointList, "jointList", null); } - } diff --git a/jme3-core/src/main/java/com/jme3/animation/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java similarity index 92% rename from jme3-core/src/main/java/com/jme3/animation/Joint.java rename to jme3-core/src/main/java/com/jme3/anim/Joint.java index a5a35af7f..3797039fe 100644 --- a/jme3-core/src/main/java/com/jme3/animation/Joint.java +++ b/jme3-core/src/main/java/com/jme3/anim/Joint.java @@ -1,4 +1,4 @@ -package com.jme3.animation; +package com.jme3.anim; import com.jme3.export.*; import com.jme3.material.MatParamOverride; @@ -23,13 +23,6 @@ public class Joint implements Savable, JmeCloneable { private ArrayList children = new ArrayList<>(); private Geometry targetGeometry; - public Joint() { - } - - public Joint(String name) { - this.name = name; - } - /** * The attachment node. */ @@ -43,29 +36,37 @@ public class Joint implements Savable, JmeCloneable { /** * The base transform of the joint in local space. - * Those transform are the bone's initial value. + * Those transform are the joint's initial value. */ private Transform baseLocalTransform = new Transform(); /** - * The transform of the bone in model space. Relative to the origin of the model. + * The transform of the joint in model space. Relative to the origin of the model. */ private Transform modelTransform = new Transform(); /** - * The matrix used to transform affected vertices position into the bone model space. + * The matrix used to transform affected vertices position into the joint model space. * Used for skinning. */ private Matrix4f inverseModelBindMatrix = new Matrix4f(); + + public Joint() { + } + + public Joint(String name) { + this.name = name; + } + /** * Updates world transforms for this bone and it's children. */ public final void update() { this.updateModelTransforms(); - for (int i = children.size() - 1; i >= 0; i--) { - children.get(i).update(); + for (Joint child : children) { + child.update(); } } @@ -128,7 +129,7 @@ public class Joint implements Savable, JmeCloneable { * * @param outTransform */ - void getOffsetTransform(Matrix4f outTransform, Quaternion tmp1, Vector3f tmp2, Vector3f tmp3, Matrix3f tmp4) { + void getOffsetTransform(Matrix4f outTransform) { modelTransform.toTransformMatrix(outTransform).mult(inverseModelBindMatrix, outTransform); } @@ -136,6 +137,19 @@ public class Joint implements Savable, JmeCloneable { //Note that the whole Armature must be updated before calling this method. modelTransform.toTransformMatrix(inverseModelBindMatrix); inverseModelBindMatrix.invertLocal(); + baseLocalTransform.set(localTransform); + } + + protected void resetToBindPose() { + localTransform.fromTransformMatrix(inverseModelBindMatrix.invert()); // local = modelBind + if (parent != null) { + localTransform.combineWithParent(parent.modelTransform.invert()); // local = local Bind + } + updateModelTransforms(); + + for (Joint child : children) { + child.resetToBindPose(); + } } public Vector3f getLocalTranslation() { diff --git a/jme3-core/src/main/java/com/jme3/anim/JointTrack.java b/jme3-core/src/main/java/com/jme3/anim/JointTrack.java new file mode 100644 index 000000000..1d2b4fd51 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/JointTrack.java @@ -0,0 +1,113 @@ +/* + * 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.anim; + +import com.jme3.export.*; +import com.jme3.math.*; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; + +/** + * Contains a list of transforms and times for each keyframe. + * + * @author Rémy Bouquet + */ +public final class JointTrack extends TransformTrack implements JmeCloneable, Savable { + + private Joint target; + + /** + * Serialization-only. Do not use. + */ + public JointTrack() { + super(); + } + + /** + * Creates a bone track for the given bone index + * + * @param target The Joint target of this track + * @param times a float array with the time of each frame + * @param translations the translation of the bone for each frame + * @param rotations the rotation of the bone for each frame + * @param scales the scale of the bone for each frame + */ + public JointTrack(Joint target, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { + super(times, translations, rotations, scales); + this.target = target; + } + + @Override + public boolean interpolate(double t) { + setDefaultTransform(target.getLocalTransform()); + boolean running = super.interpolate(t); + Transform transform = getInterpolatedTransform(); + target.setLocalTransform(transform); + return running; + } + + public void setTarget(Joint target) { + this.target = target; + } + + @Override + public Object jmeClone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException("Error cloning", e); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + super.cloneFields(cloner, original); + this.target = cloner.clone(target); + } + + @Override + public void write(JmeExporter ex) throws IOException { + super.write(ex); + OutputCapsule oc = ex.getCapsule(this); + oc.write(target, "target", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule ic = im.getCapsule(this); + target = (Joint) ic.readSavable("target", null); + } + +} diff --git a/jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java b/jme3-core/src/main/java/com/jme3/anim/SkinningControl.java similarity index 97% rename from jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java rename to jme3-core/src/main/java/com/jme3/anim/SkinningControl.java index 00f017f32..097840e81 100644 --- a/jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java +++ b/jme3-core/src/main/java/com/jme3/anim/SkinningControl.java @@ -29,7 +29,7 @@ * 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.animation; +package com.jme3.anim; import com.jme3.export.*; import com.jme3.material.MatParamOverride; @@ -53,15 +53,17 @@ import java.util.logging.Level; import java.util.logging.Logger; /** - * The Skeleton control deforms a model according to a armature, It handles the + * The Skinning control deforms a model according to an armature, It handles the * computation of the deformation matrices and performs the transformations on * the mesh + *

+ * It can perform software skinning or Hardware skinning * - * @author Rémy Bouquet Based on AnimControl by Kirill Vainer + * @author Rémy Bouquet Based on SkeletonControl by Kirill Vainer */ -public class ArmatureControl extends AbstractControl implements Cloneable, JmeCloneable { +public class SkinningControl extends AbstractControl implements Cloneable, JmeCloneable { - private static final Logger logger = Logger.getLogger(ArmatureControl.class.getName()); + private static final Logger logger = Logger.getLogger(SkinningControl.class.getName()); /** * The armature of the model. @@ -71,7 +73,7 @@ public class ArmatureControl extends AbstractControl implements Cloneable, JmeCl /** * List of geometries affected by this control. */ - private SafeArrayList targets = new SafeArrayList(Geometry.class); + private SafeArrayList targets = new SafeArrayList<>(Geometry.class); /** * Used to track when a mesh was updated. Meshes are only updated if they @@ -113,16 +115,16 @@ public class ArmatureControl extends AbstractControl implements Cloneable, JmeCl /** * Serialization only. Do not use. */ - public ArmatureControl() { + public SkinningControl() { } /** * Creates a armature control. The list of targets will be acquired * automatically when the control is attached to a node. * - * @param skeleton the armature + * @param armature the armature */ - public ArmatureControl(Armature armature) { + public SkinningControl(Armature armature) { if (armature == null) { throw new IllegalArgumentException("armature cannot be null"); } @@ -283,7 +285,7 @@ public class ArmatureControl extends AbstractControl implements Cloneable, JmeCl if (hwSkinningSupported) { hwSkinningEnabled = true; - Logger.getLogger(ArmatureControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for {0}", spatial); + Logger.getLogger(SkinningControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for {0}", spatial); } else { switchToSoftware(); } @@ -308,6 +310,7 @@ public class ArmatureControl extends AbstractControl implements Cloneable, JmeCl @Override protected void controlUpdate(float tpf) { wasMeshUpdated = false; + armature.update(); } //only do this for software updates @@ -558,10 +561,9 @@ public class ArmatureControl extends AbstractControl implements Cloneable, JmeCl * has additional indexes since tangent has 4 components instead of 3 for * pos and norm * - * @param maxWeightsPerVert maximum number of weights per vertex - * @param mesh the mesh - * @param offsetMatrices the offsetMaytrices to apply - * @param tb the tangent vertexBuffer + * @param mesh the mesh + * @param offsetMatrices the offsetMatrices to apply + * @param tb the tangent vertexBuffer */ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexBuffer tb) { int maxWeightsPerVert = mesh.getMaxNumWeights(); diff --git a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java new file mode 100644 index 000000000..5a2c329d9 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java @@ -0,0 +1,344 @@ +/* + * 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.anim; + +import com.jme3.anim.interpolator.FrameInterpolator; +import com.jme3.anim.tween.Tween; +import com.jme3.animation.CompactQuaternionArray; +import com.jme3.animation.CompactVector3Array; +import com.jme3.export.*; +import com.jme3.math.*; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; + +/** + * Contains a list of transforms and times for each keyframe. + * + * @author Rémy Bouquet + */ +public abstract class TransformTrack implements Tween, JmeCloneable, Savable { + + private double length; + + /** + * Transforms and times for track. + */ + private CompactVector3Array translations; + private CompactQuaternionArray rotations; + private CompactVector3Array scales; + private Transform transform = new Transform(); + private Transform defaultTransform = new Transform(); + private FrameInterpolator interpolator = FrameInterpolator.DEFAULT; + private float[] times; + + /** + * Serialization-only. Do not use. + */ + public TransformTrack() { + } + + /** + * Creates a transform track for the given bone index + * + * @param times a float array with the time of each frame + * @param translations the translation of the bone for each frame + * @param rotations the rotation of the bone for each frame + * @param scales the scale of the bone for each frame + */ + public TransformTrack(float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { + this.setKeyframes(times, translations, rotations, scales); + } + + /** + * Creates a bone track for the given bone index + * + * @param targetJointIndex the bone's index + */ + public TransformTrack(int targetJointIndex) { + this(); + } + + /** + * return the array of rotations of this track + * + * @return + */ + public Quaternion[] getRotations() { + return rotations.toObjectArray(); + } + + /** + * returns the array of scales for this track + * + * @return + */ + public Vector3f[] getScales() { + return scales == null ? null : scales.toObjectArray(); + } + + /** + * returns the arrays of time for this track + * + * @return + */ + public float[] getTimes() { + return times; + } + + /** + * returns the array of translations of this track + * + * @return + */ + public Vector3f[] getTranslations() { + return translations.toObjectArray(); + } + + + /** + * Sets the keyframes times for this Joint track + * + * @param times the keyframes times + */ + public void setTimes(float[] times) { + if (times.length == 0) { + throw new RuntimeException("TransformTrack with no keyframes!"); + } + this.times = times; + length = times[times.length - 1] - times[0]; + } + + /** + * Set the translations for this joint track + * + * @param translations the translation of the bone for each frame + */ + public void setKeyframesTranslation(Vector3f[] translations) { + if (times == null) { + throw new RuntimeException("TransformTrack doesn't have any time for key frames, please call setTimes first"); + } + if (translations.length == 0) { + throw new RuntimeException("TransformTrack with no translation keyframes!"); + } + this.translations = new CompactVector3Array(); + this.translations.add(translations); + this.translations.freeze(); + + assert times != null && times.length == translations.length; + } + + /** + * Set the scales for this joint track + * + * @param scales the scales of the bone for each frame + */ + public void setKeyframesScale(Vector3f[] scales) { + if (times == null) { + throw new RuntimeException("TransformTrack doesn't have any time for key frames, please call setTimes first"); + } + if (scales.length == 0) { + throw new RuntimeException("TransformTrack with no scale keyframes!"); + } + this.scales = new CompactVector3Array(); + this.scales.add(scales); + this.scales.freeze(); + + assert times != null && times.length == scales.length; + } + + /** + * Set the rotations for this joint track + * + * @param rotations the rotations of the bone for each frame + */ + public void setKeyframesRotation(Quaternion[] rotations) { + if (times == null) { + throw new RuntimeException("TransformTrack doesn't have any time for key frames, please call setTimes first"); + } + if (rotations.length == 0) { + throw new RuntimeException("TransformTrack with no rotation keyframes!"); + } + this.rotations = new CompactQuaternionArray(); + this.rotations.add(rotations); + this.rotations.freeze(); + + assert times != null && times.length == rotations.length; + } + + + /** + * Set the translations, rotations and scales for this bone track + * + * @param times a float array with the time of each frame + * @param translations the translation of the bone for each frame + * @param rotations the rotation of the bone for each frame + * @param scales the scale of the bone for each frame + */ + public void setKeyframes(float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { + setTimes(times); + if (translations != null) { + setKeyframesTranslation(translations); + } + if (rotations != null) { + setKeyframesRotation(rotations); + } + if (scales != null) { + setKeyframesScale(scales); + } + } + + @Override + public double getLength() { + return length; + } + + @Override + public boolean interpolate(double t) { + float time = (float) t; + + transform.set(defaultTransform); + int lastFrame = times.length - 1; + if (time < 0 || lastFrame == 0) { + if (translations != null) { + translations.get(0, transform.getTranslation()); + } + if (rotations != null) { + rotations.get(0, transform.getRotation()); + } + if (scales != null) { + scales.get(0, transform.getScale()); + } + return true; + } + + int startFrame = 0; + int endFrame = 1; + float blend = 0; + if (time >= times[lastFrame]) { + startFrame = lastFrame; + + time = time - times[startFrame] + times[startFrame - 1]; + blend = (time - times[startFrame - 1]) + / (times[startFrame] - times[startFrame - 1]); + + } else { + // use lastFrame so we never overflow the array + int i; + for (i = 0; i < lastFrame && times[i] < time; i++) { + startFrame = i; + endFrame = i + 1; + } + blend = (time - times[startFrame]) + / (times[endFrame] - times[startFrame]); + } + + Transform interpolated = interpolator.interpolate(blend, startFrame, translations, rotations, scales, times); + + if (translations != null) { + transform.setTranslation(interpolated.getTranslation()); + } + if (rotations != null) { + transform.setRotation(interpolated.getRotation()); + } + if (scales != null) { + transform.setScale(interpolated.getScale()); + } + + return time < length; + } + + + public Transform getInterpolatedTransform() { + return transform; + } + + public void setDefaultTransform(Transform transforms) { + defaultTransform.set(transforms); + } + + public void setFrameInterpolator(FrameInterpolator interpolator) { + this.interpolator = interpolator; + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule oc = ex.getCapsule(this); + oc.write(translations, "translations", null); + oc.write(rotations, "rotations", null); + oc.write(times, "times", null); + oc.write(scales, "scales", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + translations = (CompactVector3Array) ic.readSavable("translations", null); + rotations = (CompactQuaternionArray) ic.readSavable("rotations", null); + times = ic.readFloatArray("times", null); + scales = (CompactVector3Array) ic.readSavable("scales", null); + } + + @Override + public Object jmeClone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException("Error cloning", e); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + int tablesLength = times.length; + + times = this.times.clone(); + Vector3f[] sourceTranslations = this.getTranslations(); + Quaternion[] sourceRotations = this.getRotations(); + Vector3f[] sourceScales = this.getScales(); + + Vector3f[] translations = new Vector3f[tablesLength]; + Quaternion[] rotations = new Quaternion[tablesLength]; + Vector3f[] scales = new Vector3f[tablesLength]; + for (int i = 0; i < tablesLength; ++i) { + translations[i] = sourceTranslations[i].clone(); + rotations[i] = sourceRotations[i].clone(); + scales[i] = sourceScales != null ? sourceScales[i].clone() : new Vector3f(1.0f, 1.0f, 1.0f); + } + + setKeyframesTranslation(translations); + setKeyframesScale(scales); + setKeyframesRotation(rotations); + setFrameInterpolator(this.interpolator); + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolator.java b/jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolator.java new file mode 100644 index 000000000..a924a9d28 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolator.java @@ -0,0 +1,13 @@ +package com.jme3.anim.interpolator; + +import static com.jme3.anim.interpolator.FrameInterpolator.TrackDataReader; +import static com.jme3.anim.interpolator.FrameInterpolator.TrackTimeReader; + +/** + * Created by nehon on 15/04/17. + */ +public abstract class AnimInterpolator { + + public abstract T interpolate(float t, int currentIndex, TrackDataReader data, TrackTimeReader times, T store); + +} diff --git a/jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolators.java b/jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolators.java new file mode 100644 index 000000000..c51b73eba --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolators.java @@ -0,0 +1,151 @@ +package com.jme3.anim.interpolator; + +import com.jme3.math.*; + +import static com.jme3.anim.interpolator.FrameInterpolator.TrackDataReader; +import static com.jme3.anim.interpolator.FrameInterpolator.TrackTimeReader; + +/** + * Created by nehon on 15/04/17. + */ +public class AnimInterpolators { + + //Rotation interpolators + + public static final AnimInterpolator NLerp = new AnimInterpolator() { + private Quaternion next = new Quaternion(); + + @Override + public Quaternion interpolate(float t, int currentIndex, TrackDataReader data, TrackTimeReader times, Quaternion store) { + data.getEntryClamp(currentIndex, store); + data.getEntryClamp(currentIndex + 1, next); + store.nlerp(next, t); + return store; + } + }; + + public static final AnimInterpolator SLerp = new AnimInterpolator() { + private Quaternion next = new Quaternion(); + + @Override + public Quaternion interpolate(float t, int currentIndex, TrackDataReader data, TrackTimeReader times, Quaternion store) { + data.getEntryClamp(currentIndex, store); + data.getEntryClamp(currentIndex + 1, next); + //MathUtils.slerpNoInvert(store, next, t, store); + MathUtils.slerp(store, next, t, store); + return store; + } + }; + + public static final AnimInterpolator SQuad = new AnimInterpolator() { + private Quaternion a = new Quaternion(); + private Quaternion b = new Quaternion(); + + private Quaternion q0 = new Quaternion(); + private Quaternion q1 = new Quaternion(); + private Quaternion q2 = new Quaternion(); + private Quaternion q3 = new Quaternion(); + + @Override + public Quaternion interpolate(float t, int currentIndex, TrackDataReader data, TrackTimeReader times, Quaternion store) { + data.getEntryModSkip(currentIndex - 1, q0); + data.getEntryModSkip(currentIndex, q1); + data.getEntryModSkip(currentIndex + 1, q2); + data.getEntryModSkip(currentIndex + 2, q3); + MathUtils.squad(q0, q1, q2, q3, a, b, t, store); + return store; + } + }; + + //Position / Scale interpolators + + public static final AnimInterpolator LinearVec3f = new AnimInterpolator() { + private Vector3f next = new Vector3f(); + + @Override + public Vector3f interpolate(float t, int currentIndex, TrackDataReader data, TrackTimeReader times, Vector3f store) { + data.getEntryClamp(currentIndex, store); + data.getEntryClamp(currentIndex + 1, next); + store.interpolateLocal(next, t); + return store; + } + }; + + /** + * CatmullRom interpolation + */ + public static final CatmullRomInterpolator CatmullRom = new CatmullRomInterpolator(); + + public static class CatmullRomInterpolator extends AnimInterpolator { + private Vector3f p0 = new Vector3f(); + private Vector3f p1 = new Vector3f(); + private Vector3f p2 = new Vector3f(); + private Vector3f p3 = new Vector3f(); + private float tension = 0.7f; + + public CatmullRomInterpolator(float tension) { + this.tension = tension; + } + + public CatmullRomInterpolator() { + } + + @Override + public Vector3f interpolate(float t, int currentIndex, TrackDataReader data, TrackTimeReader times, Vector3f store) { + data.getEntryModSkip(currentIndex - 1, p0); + data.getEntryModSkip(currentIndex, p1); + data.getEntryModSkip(currentIndex + 1, p2); + data.getEntryModSkip(currentIndex + 2, p3); + + FastMath.interpolateCatmullRom(t, tension, p0, p1, p2, p3, store); + return store; + } + } + + //Time Interpolators + + public static class TimeInterpolator extends AnimInterpolator { + private EaseFunction ease; + + public TimeInterpolator(EaseFunction ease) { + this.ease = ease; + } + + @Override + public Float interpolate(float t, int currentIndex, TrackDataReader data, TrackTimeReader times, Float store) { + return ease.apply(t); + } + } + + //in + public static final TimeInterpolator easeInQuad = new TimeInterpolator(Easing.inQuad); + public static final TimeInterpolator easeInCubic = new TimeInterpolator(Easing.inCubic); + public static final TimeInterpolator easeInQuart = new TimeInterpolator(Easing.inQuart); + public static final TimeInterpolator easeInQuint = new TimeInterpolator(Easing.inQuint); + public static final TimeInterpolator easeInBounce = new TimeInterpolator(Easing.inBounce); + public static final TimeInterpolator easeInElastic = new TimeInterpolator(Easing.inElastic); + + //out + public static final TimeInterpolator easeOutQuad = new TimeInterpolator(Easing.outQuad); + public static final TimeInterpolator easeOutCubic = new TimeInterpolator(Easing.outCubic); + public static final TimeInterpolator easeOutQuart = new TimeInterpolator(Easing.outQuart); + public static final TimeInterpolator easeOutQuint = new TimeInterpolator(Easing.outQuint); + public static final TimeInterpolator easeOutBounce = new TimeInterpolator(Easing.outBounce); + public static final TimeInterpolator easeOutElastic = new TimeInterpolator(Easing.outElastic); + + //inout + public static final TimeInterpolator easeInOutQuad = new TimeInterpolator(Easing.inOutQuad); + public static final TimeInterpolator easeInOutCubic = new TimeInterpolator(Easing.inOutCubic); + public static final TimeInterpolator easeInOutQuart = new TimeInterpolator(Easing.inOutQuart); + public static final TimeInterpolator easeInOutQuint = new TimeInterpolator(Easing.inOutQuint); + public static final TimeInterpolator easeInOutBounce = new TimeInterpolator(Easing.inOutBounce); + public static final TimeInterpolator easeInOutElastic = new TimeInterpolator(Easing.inOutElastic); + + //extra + public static final TimeInterpolator smoothStep = new TimeInterpolator(Easing.smoothStep); + public static final TimeInterpolator smootherStep = new TimeInterpolator(Easing.smootherStep); + + public static final TimeInterpolator constant = new TimeInterpolator(Easing.constant); + + +} diff --git a/jme3-core/src/main/java/com/jme3/anim/interpolator/FrameInterpolator.java b/jme3-core/src/main/java/com/jme3/anim/interpolator/FrameInterpolator.java new file mode 100644 index 000000000..60d40038a --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/interpolator/FrameInterpolator.java @@ -0,0 +1,121 @@ +package com.jme3.anim.interpolator; + +import com.jme3.animation.*; +import com.jme3.math.*; + +/** + * Created by nehon on 15/04/17. + */ +public class FrameInterpolator { + + public static final FrameInterpolator DEFAULT = new FrameInterpolator(); + + private AnimInterpolator timeInterpolator; + private AnimInterpolator translationInterpolator = AnimInterpolators.LinearVec3f; + private AnimInterpolator rotationInterpolator = AnimInterpolators.NLerp; + private AnimInterpolator scaleInterpolator = AnimInterpolators.LinearVec3f; + + private TrackDataReader translationReader = new TrackDataReader<>(); + private TrackDataReader rotationReader = new TrackDataReader<>(); + private TrackDataReader scaleReader = new TrackDataReader<>(); + private TrackTimeReader timesReader = new TrackTimeReader(); + + private Transform transforms = new Transform(); + + public Transform interpolate(float t, int currentIndex, CompactVector3Array translations, CompactQuaternionArray rotations, CompactVector3Array scales, float[] times){ + timesReader.setData(times); + if( timeInterpolator != null){ + t = timeInterpolator.interpolate(t,currentIndex, null, timesReader, null ); + } + if(translations != null) { + translationReader.setData(translations); + translationInterpolator.interpolate(t, currentIndex, translationReader, timesReader, transforms.getTranslation()); + } + if(rotations != null) { + rotationReader.setData(rotations); + rotationInterpolator.interpolate(t, currentIndex, rotationReader, timesReader, transforms.getRotation()); + } + if(scales != null){ + scaleReader.setData(scales); + scaleInterpolator.interpolate(t, currentIndex, scaleReader, timesReader, transforms.getScale()); + } + return transforms; + } + + public void setTimeInterpolator(AnimInterpolator timeInterpolator) { + this.timeInterpolator = timeInterpolator; + } + + public void setTranslationInterpolator(AnimInterpolator translationInterpolator) { + this.translationInterpolator = translationInterpolator; + } + + public void setRotationInterpolator(AnimInterpolator rotationInterpolator) { + this.rotationInterpolator = rotationInterpolator; + } + + public void setScaleInterpolator(AnimInterpolator scaleInterpolator) { + this.scaleInterpolator = scaleInterpolator; + } + + + public static class TrackTimeReader { + private float[] data; + + protected void setData(float[] data) { + this.data = data; + } + + public float getEntry(int index) { + return data[mod(index, data.length)]; + } + + public int getLength() { + return data.length; + } + } + + public static class TrackDataReader { + + private CompactArray data; + + protected void setData(CompactArray data) { + this.data = data; + } + + public T getEntryMod(int index, T store) { + return data.get(mod(index, data.getTotalObjectSize()), store); + } + + public T getEntryClamp(int index, T store) { + index = (int) FastMath.clamp(index, 0, data.getTotalObjectSize() - 1); + return data.get(index, store); + } + + public T getEntryModSkip(int index, T store) { + int total = data.getTotalObjectSize(); + if (index == -1) { + index--; + } else if (index >= total) { + index++; + } + + index = mod(index, total); + + + return data.get(index, store); + } + } + + /** + * Euclidean modulo (cycle on 0,n instead of -n,0; 0,n) + * + * @param val + * @param n + * @return + */ + private static int mod(int val, int n) { + return ((val % n) + n) % n; + } + +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java b/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java new file mode 100644 index 000000000..aa3407268 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java @@ -0,0 +1,110 @@ +/* + * $Id$ + * + * Copyright (c) 2015, Simsilica, LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. 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. + * + * 3. Neither the name of the copyright holder 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 HOLDER 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.anim.tween; + + +import com.jme3.export.*; + +import java.io.IOException; + +/** + * Base implementation of the Tween interface that provides + * default implementations of the getLength() and interopolate() + * methods that provide common tween clamping and bounds checking. + * Subclasses need only override the doInterpolate() method and + * the rest is handled for them. + * + * @author Paul Speed + */ +public abstract class AbstractTween implements Tween { + + private double length; + + protected AbstractTween(double length) { + this.length = length; + } + + @Override + public double getLength() { + return length; + } + + public void setLength(double length) { + this.length = length; + } + + /** + * Default implementation clamps the time value, converts + * it to 0 to 1.0 based on getLength(), and calls doInterpolate(). + */ + @Override + public boolean interpolate(double t) { + if (t < 0) { + return true; + } + + // Scale t to be between 0 and 1 for our length + if (length == 0) { + t = 1; + } else { + t = t / length; + } + + boolean done = false; + if (t >= 1.0) { + t = 1.0; + done = true; + } + doInterpolate(t); + return !done; + } + + protected abstract void doInterpolate(double t); + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule oc = ex.getCapsule(this); + oc.write(length, "length", 0); + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + length = ic.readDouble("length", 0); + } +} + \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/Tween.java b/jme3-core/src/main/java/com/jme3/anim/tween/Tween.java new file mode 100644 index 000000000..450eb5f44 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/Tween.java @@ -0,0 +1,71 @@ +/* + * $Id$ + * + * Copyright (c) 2015, Simsilica, LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. 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. + * + * 3. Neither the name of the copyright holder 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 HOLDER 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.anim.tween; + + +import com.jme3.export.Savable; + +/** + * Represents some action that interpolates across input between 0 + * and some length value. (For example, movement, rotation, fading.) + * It's also possible to have zero length 'instant' tweens. + * + * @author Paul Speed + */ +public interface Tween extends Savable, Cloneable { + + /** + * Returns the length of the tween. If 't' represents time in + * seconds then this is the notional time in seconds that the tween + * will run. Note: all of the caveats are because tweens may be + * externally scaled in such a way that 't' no longer represents + * actual time. + */ + public double getLength(); + + /** + * Sets the implementation specific interpolation to the + * specified 'tween' value as a value in the range from 0 to + * getLength(). If the value is greater or equal to getLength() + * then it is internally clamped and the method returns false. + * If 't' is still in the tween's range then this method returns + * true. + */ + public boolean interpolate(double t); + +} + diff --git a/jme3-core/src/main/java/com/jme3/animation/AnimChannel.java b/jme3-core/src/main/java/com/jme3/animation/AnimChannel.java index 0cd24cab8..04c8c2f49 100644 --- a/jme3-core/src/main/java/com/jme3/animation/AnimChannel.java +++ b/jme3-core/src/main/java/com/jme3/animation/AnimChannel.java @@ -33,6 +33,7 @@ package com.jme3.animation; import com.jme3.math.FastMath; import com.jme3.util.TempVars; + import java.util.BitSet; /** @@ -46,6 +47,7 @@ import java.util.BitSet; * * @author Kirill Vainer */ +@Deprecated public final class AnimChannel { private static final float DEFAULT_BLEND_TIME = 0.15f; diff --git a/jme3-core/src/main/java/com/jme3/animation/AnimControl.java b/jme3-core/src/main/java/com/jme3/animation/AnimControl.java index 8718cde26..7386e1084 100644 --- a/jme3-core/src/main/java/com/jme3/animation/AnimControl.java +++ b/jme3-core/src/main/java/com/jme3/animation/AnimControl.java @@ -64,7 +64,9 @@ import java.util.Map; * 1) Morph/Pose animation * * @author Kirill Vainer + * @deprecated use {@link com.jme3.anim.AnimComposer} */ +@Deprecated public final class AnimControl extends AbstractControl implements Cloneable, JmeCloneable { /** diff --git a/jme3-core/src/main/java/com/jme3/animation/AnimEventListener.java b/jme3-core/src/main/java/com/jme3/animation/AnimEventListener.java index d9b48459a..c20f150da 100644 --- a/jme3-core/src/main/java/com/jme3/animation/AnimEventListener.java +++ b/jme3-core/src/main/java/com/jme3/animation/AnimEventListener.java @@ -37,6 +37,7 @@ package com.jme3.animation; * * @author Kirill Vainer */ +@Deprecated public interface AnimEventListener { /** diff --git a/jme3-core/src/main/java/com/jme3/animation/Animation.java b/jme3-core/src/main/java/com/jme3/animation/Animation.java index 92a5a38fc..834595238 100644 --- a/jme3-core/src/main/java/com/jme3/animation/Animation.java +++ b/jme3-core/src/main/java/com/jme3/animation/Animation.java @@ -37,13 +37,16 @@ import com.jme3.util.SafeArrayList; import com.jme3.util.TempVars; import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; + import java.io.IOException; /** * The animation class updates the animation target with the tracks of a given type. * * @author Kirill Vainer, Marcin Roguski (Kaelthas) + * @deprecated use {@link com.jme3.anim.AnimClip} */ +@Deprecated public class Animation implements Savable, Cloneable, JmeCloneable { /** diff --git a/jme3-core/src/main/java/com/jme3/animation/AudioTrack.java b/jme3-core/src/main/java/com/jme3/animation/AudioTrack.java index f4c53e4f1..300287c1e 100644 --- a/jme3-core/src/main/java/com/jme3/animation/AudioTrack.java +++ b/jme3-core/src/main/java/com/jme3/animation/AudioTrack.java @@ -32,15 +32,12 @@ package com.jme3.animation; import com.jme3.audio.AudioNode; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; +import com.jme3.export.*; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.util.TempVars; import com.jme3.util.clone.Cloner; -import com.jme3.util.clone.JmeCloneable; + import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -62,6 +59,7 @@ import java.util.logging.Logger; * * @author Nehon */ +@Deprecated public class AudioTrack implements ClonableTrack { private static final Logger logger = Logger.getLogger(AudioTrack.class.getName()); diff --git a/jme3-core/src/main/java/com/jme3/animation/Bone.java b/jme3-core/src/main/java/com/jme3/animation/Bone.java index e055f2e46..6cd751e3c 100644 --- a/jme3-core/src/main/java/com/jme3/animation/Bone.java +++ b/jme3-core/src/main/java/com/jme3/animation/Bone.java @@ -67,7 +67,9 @@ import java.util.ArrayList; * * @author Kirill Vainer * @author Rémy Bouquet + * @deprecated use {@link com.jme3.anim.Joint} */ +@Deprecated public final class Bone implements Savable, JmeCloneable { // Version #2: Changed naming of transforms as they were misleading diff --git a/jme3-core/src/main/java/com/jme3/animation/BoneTrack.java b/jme3-core/src/main/java/com/jme3/animation/BoneTrack.java index 1eca2e9dc..32f5e6b7b 100644 --- a/jme3-core/src/main/java/com/jme3/animation/BoneTrack.java +++ b/jme3-core/src/main/java/com/jme3/animation/BoneTrack.java @@ -35,8 +35,12 @@ import com.jme3.export.*; import com.jme3.math.Quaternion; import com.jme3.math.Vector3f; import com.jme3.util.TempVars; +<<<<<<< HEAD import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; +======= + +>>>>>>> Draft of the new animation system import java.io.IOException; import java.util.BitSet; @@ -44,7 +48,9 @@ import java.util.BitSet; * Contains a list of transforms and times for each keyframe. * * @author Kirill Vainer + * @deprecated use {@link com.jme3.anim.JointTrack} */ +@Deprecated public final class BoneTrack implements JmeCloneable, Track { /** diff --git a/jme3-core/src/main/java/com/jme3/animation/ClonableTrack.java b/jme3-core/src/main/java/com/jme3/animation/ClonableTrack.java index 6dd94c806..b777203a5 100644 --- a/jme3-core/src/main/java/com/jme3/animation/ClonableTrack.java +++ b/jme3-core/src/main/java/com/jme3/animation/ClonableTrack.java @@ -44,6 +44,7 @@ import com.jme3.util.clone.JmeCloneable; * * @author Nehon */ +@Deprecated public interface ClonableTrack extends Track, JmeCloneable { /** diff --git a/jme3-core/src/main/java/com/jme3/animation/EffectTrack.java b/jme3-core/src/main/java/com/jme3/animation/EffectTrack.java index efc5b9fbb..a05b481c4 100644 --- a/jme3-core/src/main/java/com/jme3/animation/EffectTrack.java +++ b/jme3-core/src/main/java/com/jme3/animation/EffectTrack.java @@ -32,10 +32,7 @@ package com.jme3.animation; import com.jme3.effect.ParticleEmitter; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; +import com.jme3.export.*; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.Node; @@ -67,6 +64,7 @@ import java.util.logging.Logger; * * @author Nehon */ +@Deprecated public class EffectTrack implements ClonableTrack { private static final Logger logger = Logger.getLogger(EffectTrack.class.getName()); @@ -130,15 +128,17 @@ public class EffectTrack implements ClonableTrack { @Override protected void controlRender(RenderManager rm, ViewPort vp) { } - }; + } //Anim listener that stops the Emmitter when the animation is finished or changed. private class OnEndListener implements AnimEventListener { + @Override public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { stop(); } + @Override public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { } } @@ -188,6 +188,7 @@ public class EffectTrack implements ClonableTrack { * @see Track#setTime(float, float, com.jme3.animation.AnimControl, * com.jme3.animation.AnimChannel, com.jme3.util.TempVars) */ + @Override public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars) { if (time >= length) { @@ -233,6 +234,7 @@ public class EffectTrack implements ClonableTrack { * * @return length of the track */ + @Override public float getLength() { return length; } @@ -325,6 +327,7 @@ public class EffectTrack implements ClonableTrack { return null; } + @Override public void cleanUp() { TrackInfo t = (TrackInfo) emitter.getUserData("TrackInfo"); t.getTracks().remove(this); @@ -413,6 +416,7 @@ public class EffectTrack implements ClonableTrack { * @param ex exporter * @throws IOException exception */ + @Override public void write(JmeExporter ex) throws IOException { OutputCapsule out = ex.getCapsule(this); //reset the particle emission rate on the emitter before saving. @@ -431,6 +435,7 @@ public class EffectTrack implements ClonableTrack { * @param im importer * @throws IOException Exception */ + @Override public void read(JmeImporter im) throws IOException { InputCapsule in = im.getCapsule(this); this.particlesPerSeconds = in.readFloat("particlesPerSeconds", 0); diff --git a/jme3-core/src/main/java/com/jme3/animation/LoopMode.java b/jme3-core/src/main/java/com/jme3/animation/LoopMode.java index ca7b1eb1c..16ca1deb0 100644 --- a/jme3-core/src/main/java/com/jme3/animation/LoopMode.java +++ b/jme3-core/src/main/java/com/jme3/animation/LoopMode.java @@ -35,6 +35,7 @@ package com.jme3.animation; * LoopMode determines how animations repeat, or if they * do not repeat. */ +@Deprecated public enum LoopMode { /** * The animation will play repeatedly, when it reaches the end diff --git a/jme3-core/src/main/java/com/jme3/animation/Pose.java b/jme3-core/src/main/java/com/jme3/animation/Pose.java index fc06d0d07..f5d0e305a 100644 --- a/jme3-core/src/main/java/com/jme3/animation/Pose.java +++ b/jme3-core/src/main/java/com/jme3/animation/Pose.java @@ -34,12 +34,14 @@ package com.jme3.animation; import com.jme3.export.*; import com.jme3.math.Vector3f; import com.jme3.util.BufferUtils; + import java.io.IOException; import java.nio.FloatBuffer; /** * A pose is a list of offsets that say where a mesh vertices should be for this pose. */ +@Deprecated public final class Pose implements Savable, Cloneable { private String name; diff --git a/jme3-core/src/main/java/com/jme3/animation/Skeleton.java b/jme3-core/src/main/java/com/jme3/animation/Skeleton.java index bff876790..651bf06a3 100644 --- a/jme3-core/src/main/java/com/jme3/animation/Skeleton.java +++ b/jme3-core/src/main/java/com/jme3/animation/Skeleton.java @@ -31,11 +31,13 @@ */ package com.jme3.animation; +import com.jme3.anim.Armature; import com.jme3.export.*; import com.jme3.math.Matrix4f; import com.jme3.util.TempVars; -import com.jme3.util.clone.JmeCloneable; import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -46,7 +48,9 @@ import java.util.List; * animated matrixes. * * @author Kirill Vainer + * @deprecated use {@link Armature} */ +@Deprecated public final class Skeleton implements Savable, JmeCloneable { private Bone[] rootBones; diff --git a/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java b/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java index 1e9abea5b..06f6927ab 100644 --- a/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java +++ b/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java @@ -31,6 +31,7 @@ */ package com.jme3.animation; +import com.jme3.anim.SkinningControl; import com.jme3.export.*; import com.jme3.material.MatParamOverride; import com.jme3.math.FastMath; @@ -57,7 +58,9 @@ import java.util.logging.Logger; * the mesh * * @author Rémy Bouquet Based on AnimControl by Kirill Vainer + * @deprecated use {@link SkinningControl} */ +@Deprecated public class SkeletonControl extends AbstractControl implements Cloneable, JmeCloneable { /** diff --git a/jme3-core/src/main/java/com/jme3/animation/SpatialTrack.java b/jme3-core/src/main/java/com/jme3/animation/SpatialTrack.java index 5afd84350..fc992d2b0 100644 --- a/jme3-core/src/main/java/com/jme3/animation/SpatialTrack.java +++ b/jme3-core/src/main/java/com/jme3/animation/SpatialTrack.java @@ -31,10 +31,7 @@ */ package com.jme3.animation; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; +import com.jme3.export.*; import com.jme3.math.Quaternion; import com.jme3.math.Vector3f; import com.jme3.scene.Spatial; @@ -48,8 +45,9 @@ import java.io.IOException; * * @author Marcin Roguski (Kaelthas) */ +@Deprecated public class SpatialTrack implements JmeCloneable, Track { - + /** * Translations of the track. */ diff --git a/jme3-core/src/main/java/com/jme3/animation/Track.java b/jme3-core/src/main/java/com/jme3/animation/Track.java index 4a6dcdef9..7777e8894 100644 --- a/jme3-core/src/main/java/com/jme3/animation/Track.java +++ b/jme3-core/src/main/java/com/jme3/animation/Track.java @@ -34,6 +34,7 @@ package com.jme3.animation; import com.jme3.export.Savable; import com.jme3.util.TempVars; +@Deprecated public interface Track extends Savable, Cloneable { /** diff --git a/jme3-core/src/main/java/com/jme3/animation/TrackInfo.java b/jme3-core/src/main/java/com/jme3/animation/TrackInfo.java index 7ee927cd3..93fd6a88e 100644 --- a/jme3-core/src/main/java/com/jme3/animation/TrackInfo.java +++ b/jme3-core/src/main/java/com/jme3/animation/TrackInfo.java @@ -31,13 +31,10 @@ */ package com.jme3.animation; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; -import com.jme3.export.Savable; +import com.jme3.export.*; import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; + import java.io.IOException; import java.util.ArrayList; @@ -50,6 +47,7 @@ import java.util.ArrayList; * * @author Nehon */ +@Deprecated public class TrackInfo implements Savable, JmeCloneable { ArrayList tracks = new ArrayList(); diff --git a/jme3-core/src/main/java/com/jme3/math/EaseFunction.java b/jme3-core/src/main/java/com/jme3/math/EaseFunction.java new file mode 100644 index 000000000..c2f5383f2 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/math/EaseFunction.java @@ -0,0 +1,13 @@ +package com.jme3.math; + +/** + * Created by Nehon on 26/03/2017. + */ +public interface EaseFunction { + + /** + * @param value a value from 0 to 1. Passing a value out of this range will have unexpected behavior. + * @return + */ + float apply(float value); +} diff --git a/jme3-core/src/main/java/com/jme3/math/Easing.java b/jme3-core/src/main/java/com/jme3/math/Easing.java new file mode 100644 index 000000000..e9969c91a --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/math/Easing.java @@ -0,0 +1,163 @@ +package com.jme3.math; + +/** + * Expose several Easing function from Robert Penner + * Created by Nehon on 26/03/2017. + */ +public class Easing { + + + public static EaseFunction constant = new EaseFunction() { + @Override + public float apply(float value) { + return 0; + } + }; + /** + * In + */ + public static EaseFunction linear = new EaseFunction() { + @Override + public float apply(float value) { + return value; + } + }; + + public static EaseFunction inQuad = new EaseFunction() { + @Override + public float apply(float value) { + return value * value; + } + }; + + public static EaseFunction inCubic = new EaseFunction() { + @Override + public float apply(float value) { + return value * value * value; + } + }; + + public static EaseFunction inQuart = new EaseFunction() { + @Override + public float apply(float value) { + return value * value * value * value; + } + }; + + public static EaseFunction inQuint = new EaseFunction() { + @Override + public float apply(float value) { + return value * value * value * value * value; + } + }; + + + /** + * Out Elastic and bounce + */ + public static EaseFunction outElastic = new EaseFunction() { + @Override + public float apply(float value) { + return FastMath.pow(2f, -10f * value) * FastMath.sin((value - 0.3f / 4f) * (2f * FastMath.PI) / 0.3f) + 1f; + } + }; + + public static EaseFunction outBounce = new EaseFunction() { + @Override + public float apply(float value) { + if (value < (1f / 2.75f)) { + return (7.5625f * value * value); + } else if (value < (2f / 2.75f)) { + return (7.5625f * (value -= (1.5f / 2.75f)) * value + 0.75f); + } else if (value < (2.5 / 2.75)) { + return (7.5625f * (value -= (2.25f / 2.75f)) * value + 0.9375f); + } else { + return (7.5625f * (value -= (2.625f / 2.75f)) * value + 0.984375f); + } + } + }; + + /** + * In Elastic and bounce + */ + public static EaseFunction inElastic = new Invert(outElastic); + public static EaseFunction inBounce = new Invert(outBounce); + + /** + * Out + */ + public static EaseFunction outQuad = new Invert(inQuad); + public static EaseFunction outCubic = new Invert(inCubic); + public static EaseFunction outQuart = new Invert(inQuart); + public static EaseFunction outQuint = new Invert(inQuint); + + /** + * inOut + */ + public static EaseFunction inOutQuad = new InOut(inQuad, outQuad); + public static EaseFunction inOutCubic = new InOut(inCubic, outCubic); + public static EaseFunction inOutQuart = new InOut(inQuart, outQuart); + public static EaseFunction inOutQuint = new InOut(inQuint, outQuint); + public static EaseFunction inOutElastic = new InOut(inElastic, outElastic); + public static EaseFunction inOutBounce = new InOut(inBounce, outBounce); + + + /** + * Extra functions + */ + + public static EaseFunction smoothStep = new EaseFunction() { + @Override + public float apply(float t) { + return t * t * (3f - 2f * t); + } + }; + + public static EaseFunction smootherStep = new EaseFunction() { + @Override + public float apply(float t) { + return t * t * t * (t * (t * 6f - 15f) + 10f); + } + }; + + /** + * An Ease function composed of 2 sb function for custom in and out easing + */ + public static class InOut implements EaseFunction { + + private EaseFunction in; + private EaseFunction out; + + public InOut(EaseFunction in, EaseFunction out) { + this.in = in; + this.out = out; + } + + @Override + public float apply(float value) { + if (value < 0.5) { + value = value * 2; + return inQuad.apply(value) / 2; + } else { + value = (value - 0.5f) * 2; + return outQuad.apply(value) / 2 + 0.5f; + } + } + } + + private static class Invert implements EaseFunction { + + private EaseFunction func; + + public Invert(EaseFunction func) { + this.func = func; + } + + @Override + public float apply(float value) { + return 1f - func.apply(1f - value); + } + } + + +} diff --git a/jme3-core/src/main/java/com/jme3/math/MathUtils.java b/jme3-core/src/main/java/com/jme3/math/MathUtils.java new file mode 100644 index 000000000..a27c266ce --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/math/MathUtils.java @@ -0,0 +1,165 @@ +package com.jme3.math; + +/** + * Created by Nehon on 23/04/2017. + */ +public class MathUtils { + + public static Quaternion log(Quaternion q, Quaternion store) { + float a = FastMath.acos(q.w); + float sina = FastMath.sin(a); + + store.w = 0; + if (sina > 0) { + store.x = a * q.x / sina; + store.y = a * q.y / sina; + store.z = a * q.z / sina; + } else { + store.x = 0; + store.y = 0; + store.z = 0; + } + return store; + } + + public static Quaternion exp(Quaternion q, Quaternion store) { + + float len = FastMath.sqrt(q.x * q.x + q.y * q.y + q.z * q.z); + float sinLen = FastMath.sin(len); + float cosLen = FastMath.cos(len); + + store.w = cosLen; + if (len > 0) { + store.x = sinLen * q.x / len; + store.y = sinLen * q.y / len; + store.z = sinLen * q.z / len; + } else { + store.x = 0; + store.y = 0; + store.z = 0; + } + return store; + } + + //! This version of slerp, used by squad, does not check for theta > 90. + public static Quaternion slerpNoInvert(Quaternion q1, Quaternion q2, float t, Quaternion store) { + float dot = q1.dot(q2); + + if (dot > -0.95f && dot < 0.95f) { + float angle = FastMath.acos(dot); + float sin1 = FastMath.sin(angle * (1 - t)); + float sin2 = FastMath.sin(angle * t); + float sin3 = FastMath.sin(angle); + store.x = (q1.x * sin1 + q2.x * sin2) / sin3; + store.y = (q1.y * sin1 + q2.y * sin2) / sin3; + store.z = (q1.z * sin1 + q2.z * sin2) / sin3; + store.w = (q1.w * sin1 + q2.w * sin2) / sin3; + System.err.println("real slerp"); + } else { + // if the angle is small, use linear interpolation + store.set(q1).nlerp(q2, t); + System.err.println("nlerp"); + } + return store; + } + + public static Quaternion slerp(Quaternion q1, Quaternion q2, float t, Quaternion store) { + + float dot = (q1.x * q2.x) + (q1.y * q2.y) + (q1.z * q2.z) + + (q1.w * q2.w); + + if (dot < 0.0f) { + // Negate the second quaternion and the result of the dot product + q2.x = -q2.x; + q2.y = -q2.y; + q2.z = -q2.z; + q2.w = -q2.w; + dot = -dot; + } + + // Set the first and second scale for the interpolation + float scale0 = 1 - t; + float scale1 = t; + + // Check if the angle between the 2 quaternions was big enough to + // warrant such calculations + if (dot < 0.9f) {// Get the angle between the 2 quaternions, + // and then store the sin() of that angle + float theta = FastMath.acos(dot); + float invSinTheta = 1f / FastMath.sin(theta); + + // Calculate the scale for q1 and q2, according to the angle and + // it's sine value + scale0 = FastMath.sin((1 - t) * theta) * invSinTheta; + scale1 = FastMath.sin((t * theta)) * invSinTheta; + + // Calculate the x, y, z and w values for the quaternion by using a + // special + // form of linear interpolation for quaternions. + store.x = (scale0 * q1.x) + (scale1 * q2.x); + store.y = (scale0 * q1.y) + (scale1 * q2.y); + store.z = (scale0 * q1.z) + (scale1 * q2.z); + store.w = (scale0 * q1.w) + (scale1 * q2.w); + } else { + store.x = (scale0 * q1.x) + (scale1 * q2.x); + store.y = (scale0 * q1.y) + (scale1 * q2.y); + store.z = (scale0 * q1.z) + (scale1 * q2.z); + store.w = (scale0 * q1.w) + (scale1 * q2.w); + store.normalizeLocal(); + } + // Return the interpolated quaternion + return store; + } + +// //! Given 3 quaternions, qn-1,qn and qn+1, calculate a control point to be used in spline interpolation +// private static Quaternion spline(Quaternion qnm1, Quaternion qn, Quaternion qnp1, Quaternion store, Quaternion tmp) { +// store.set(-qn.x, -qn.y, -qn.z, qn.w); +// //store.set(qn).inverseLocal(); +// tmp.set(store); +// +// log(store.multLocal(qnm1), store); +// log(tmp.multLocal(qnp1), tmp); +// store.addLocal(tmp).multLocal(1f / -4f); +// exp(store, tmp); +// store.set(tmp).multLocal(qn); +// +// return store.normalizeLocal(); +// //return qn * (((qni * qnm1).log() + (qni * qnp1).log()) / -4).exp(); +// } + + //! Given 3 quaternions, qn-1,qn and qn+1, calculate a control point to be used in spline interpolation + private static Quaternion spline(Quaternion qnm1, Quaternion qn, Quaternion qnp1, Quaternion store, Quaternion tmp) { + Quaternion invQn = new Quaternion(-qn.x, -qn.y, -qn.z, qn.w); + + + log(invQn.mult(qnp1), tmp); + log(invQn.mult(qnm1), store); + store.addLocal(tmp).multLocal(-1f / 4f); + exp(store, tmp); + store.set(qn).multLocal(tmp); + + return store.normalizeLocal(); + //return qn * (((qni * qnm1).log() + (qni * qnp1).log()) / -4).exp(); + } + + + //! spherical cubic interpolation + public static Quaternion squad(Quaternion q0, Quaternion q1, Quaternion q2, Quaternion q3, Quaternion a, Quaternion b, float t, Quaternion store) { + + spline(q0, q1, q2, a, store); + spline(q1, q2, q3, b, store); + + slerp(a, b, t, store); + slerp(q1, q2, t, a); + return slerp(a, store, 2 * t * (1 - t), b); + //slerpNoInvert(a, b, t, store); + //slerpNoInvert(q1, q2, t, a); + //return slerpNoInvert(a, store, 2 * t * (1 - t), b); + +// quaternion c = slerpNoInvert(q1, q2, t), +// d = slerpNoInvert(a, b, t); +// return slerpNoInvert(c, d, 2 * t * (1 - t)); + } + + +} diff --git a/jme3-core/src/main/java/com/jme3/math/Transform.java b/jme3-core/src/main/java/com/jme3/math/Transform.java index f0a18bf91..6c8795a4a 100644 --- a/jme3-core/src/main/java/com/jme3/math/Transform.java +++ b/jme3-core/src/main/java/com/jme3/math/Transform.java @@ -181,7 +181,8 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable * @param delta An amount between 0 and 1 representing how far to interpolate from t1 to t2. */ public void interpolateTransforms(Transform t1, Transform t2, float delta) { - this.rot.slerp(t1.rot,t2.rot,delta); + t1.rot.nlerp(t2.rot, delta); + this.rot.set(t1.rot); this.translation.interpolateLocal(t1.translation,t2.translation,delta); this.scale.interpolateLocal(t1.scale,t2.scale,delta); } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java index b0b6e7e27..3e3149f59 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java @@ -32,8 +32,8 @@ package com.jme3.scene.debug.custom; * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import com.jme3.animation.Armature; -import com.jme3.animation.Joint; +import com.jme3.anim.Armature; +import com.jme3.anim.Joint; import com.jme3.bounding.*; import com.jme3.math.Quaternion; import com.jme3.math.Vector3f; diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java index 73d0a7b01..06661eff7 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java @@ -4,7 +4,7 @@ */ package com.jme3.scene.debug.custom; -import com.jme3.animation.*; +import com.jme3.anim.*; import com.jme3.app.Application; import com.jme3.app.state.AbstractAppState; import com.jme3.app.state.AppStateManager; @@ -55,9 +55,9 @@ public class ArmatureDebugAppState extends AbstractAppState { debugNode.updateGeometricState(); } - public ArmatureDebugger addArmature(ArmatureControl armatureControl, boolean guessJointsOrientation) { - Armature armature = armatureControl.getArmature(); - Spatial forSpatial = armatureControl.getSpatial(); + public ArmatureDebugger addArmature(SkinningControl skinningControl, boolean guessJointsOrientation) { + Armature armature = skinningControl.getArmature(); + Spatial forSpatial = skinningControl.getSpatial(); return addArmature(armature, forSpatial, guessJointsOrientation); } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java index 7899cabac..8d068c185 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java @@ -32,7 +32,9 @@ package com.jme3.scene.debug.custom; * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import com.jme3.animation.*; +import com.jme3.anim.Armature; +import com.jme3.anim.Joint; +import com.jme3.animation.Bone; import com.jme3.asset.AssetManager; import com.jme3.material.Material; import com.jme3.math.ColorRGBA; @@ -91,7 +93,7 @@ public class ArmatureDebugger extends BatchNode { interJointWires = new ArmatureInterJointsWire(armature, bonesLength, guessJointsOrientation); wires = new Geometry(name + "_interwires", interJointWires); - this.attachChild(wires); + // this.attachChild(wires); } protected void initialize(AssetManager assetManager) { @@ -152,7 +154,7 @@ public class ArmatureDebugger extends BatchNode { super.updateLogicalState(tpf); bones.updateGeometry(); if (interJointWires != null) { - interJointWires.updateGeometry(); + // interJointWires.updateGeometry(); } } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java index fbd97c1e0..3c93e7dad 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java @@ -33,8 +33,8 @@ package com.jme3.scene.debug.custom; */ -import com.jme3.animation.Armature; -import com.jme3.animation.Joint; +import com.jme3.anim.Armature; +import com.jme3.anim.Joint; import com.jme3.math.Vector3f; import com.jme3.scene.Mesh; import com.jme3.scene.VertexBuffer; diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index d3d0da624..1623977c1 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -43,11 +43,12 @@ import com.jme3.renderer.Limits; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.scene.control.Control; -import com.jme3.scene.debug.custom.SkeletonDebugAppState; import java.util.ArrayList; import java.util.List; +//import com.jme3.scene.debug.custom.SkeletonDebugAppState; + public class TestGltfLoading extends SimpleApplication { Node autoRotate = new Node("autoRotate"); @@ -74,8 +75,8 @@ public class TestGltfLoading extends SimpleApplication { */ public void simpleInitApp() { - SkeletonDebugAppState skeletonDebugAppState = new SkeletonDebugAppState(); - getStateManager().attach(skeletonDebugAppState); +// SkeletonDebugAppState skeletonDebugAppState = new SkeletonDebugAppState(); +// getStateManager().attach(skeletonDebugAppState); String folder = System.getProperty("user.home"); assetManager.registerLocator(folder, FileLocator.class); @@ -112,7 +113,7 @@ public class TestGltfLoading extends SimpleApplication { // loadModel("Models/gltf/box/box.gltf", Vector3f.ZERO, 1); // loadModel("Models/gltf/duck/Duck.gltf", new Vector3f(0, -1, 0), 1); // loadModel("Models/gltf/damagedHelmet/damagedHelmet.gltf", Vector3f.ZERO, 1); -// loadModel("Models/gltf/hornet/scene.gltf", new Vector3f(0, -0.5f, 0), 0.4f); + loadModel("Models/gltf/hornet/scene.gltf", new Vector3f(0, -0.5f, 0), 0.4f); //// loadModel("Models/gltf/adamHead/adamHead.gltf", Vector3f.ZERO, 0.6f); // loadModel("Models/gltf/busterDrone/busterDrone.gltf", new Vector3f(0, 0f, 0), 0.8f); // loadModel("Models/gltf/animatedCube/AnimatedCube.gltf", Vector3f.ZERO, 0.5f); @@ -206,14 +207,14 @@ public class TestGltfLoading extends SimpleApplication { return; } ctrl.setHardwareSkinningPreferred(false); - getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(ctrl, true); + //getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(ctrl, true); // AnimControl aCtrl = findControl(s, AnimControl.class); // //ctrl.getSpatial().removeControl(ctrl); // if (aCtrl == null) { // return; // } -// if (aCtrl.getSkeleton() != null) { -// getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(aCtrl.getSkeleton(), aCtrl.getSpatial(), true); +// if (aCtrl.getArmature() != null) { +// getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(aCtrl.getArmature(), aCtrl.getSpatial(), true); // } } diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading2.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading2.java index 7988da4fa..d467fbc9b 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading2.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading2.java @@ -42,11 +42,12 @@ import com.jme3.renderer.Limits; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.scene.control.Control; -import com.jme3.scene.debug.custom.SkeletonDebugAppState; import com.jme3.scene.plugins.gltf.GltfModelKey; import java.util.*; +//import com.jme3.scene.debug.custom.SkeletonDebugAppState; + public class TestGltfLoading2 extends SimpleApplication { Node autoRotate = new Node("autoRotate"); @@ -73,8 +74,8 @@ public class TestGltfLoading2 extends SimpleApplication { */ public void simpleInitApp() { - SkeletonDebugAppState skeletonDebugAppState = new SkeletonDebugAppState(); - getStateManager().attach(skeletonDebugAppState); +// SkeletonDebugAppState skeletonDebugAppState = new SkeletonDebugAppState(); +// getStateManager().attach(skeletonDebugAppState); // cam.setLocation(new Vector3f(4.0339394f, 2.645184f, 6.4627485f)); // cam.setRotation(new Quaternion(-0.013950467f, 0.98604023f, -0.119502485f, -0.11510504f)); @@ -226,7 +227,7 @@ public class TestGltfLoading2 extends SimpleApplication { if (ctrl == null) { return; } - //System.err.println(ctrl.getSkeleton().toString()); + //System.err.println(ctrl.getArmature().toString()); //ctrl.setHardwareSkinningPreferred(false); // getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(ctrl, true); // AnimControl aCtrl = findControl(s, AnimControl.class); @@ -234,8 +235,8 @@ public class TestGltfLoading2 extends SimpleApplication { // if (aCtrl == null) { // return; // } -// if (aCtrl.getSkeleton() != null) { -// getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(aCtrl.getSkeleton(), aCtrl.getSpatial(), true); +// if (aCtrl.getArmature() != null) { +// getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(aCtrl.getArmature(), aCtrl.getSpatial(), true); // } } diff --git a/jme3-examples/src/main/java/jme3test/model/anim/EraseTimer.java b/jme3-examples/src/main/java/jme3test/model/anim/EraseTimer.java new file mode 100644 index 000000000..ba6189195 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/anim/EraseTimer.java @@ -0,0 +1,76 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package jme3test.model.anim; + +import com.jme3.system.Timer; + +/** + * @author Nehon + */ +public class EraseTimer extends Timer { + + + //private static final long TIMER_RESOLUTION = 1000L; + //private static final float INVERSE_TIMER_RESOLUTION = 1f/1000L; + private static final long TIMER_RESOLUTION = 1000000000L; + private static final float INVERSE_TIMER_RESOLUTION = 1f / 1000000000L; + + private long startTime; + private long previousTime; + private float tpf; + private float fps; + + public EraseTimer() { + //startTime = System.currentTimeMillis(); + startTime = System.nanoTime(); + } + + /** + * Returns the time in seconds. The timer starts + * at 0.0 seconds. + * + * @return the current time in seconds + */ + @Override + public float getTimeInSeconds() { + return getTime() * INVERSE_TIMER_RESOLUTION; + } + + public long getTime() { + //return System.currentTimeMillis() - startTime; + return System.nanoTime() - startTime; + } + + public long getResolution() { + return TIMER_RESOLUTION; + } + + public float getFrameRate() { + return fps; + } + + public float getTimePerFrame() { + return tpf; + } + + public void update() { + tpf = (getTime() - previousTime) * (1.0f / TIMER_RESOLUTION); + if (tpf >= 0.2) { + //the frame lasted more than 200ms we erase its time to 16ms. + tpf = 0.016666f; + } else { + fps = 1.0f / tpf; + } + previousTime = getTime(); + } + + public void reset() { + //startTime = System.currentTimeMillis(); + startTime = System.nanoTime(); + previousTime = getTime(); + } + + +} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java index c4f23e882..0beb2d0ae 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java @@ -1,15 +1,22 @@ package jme3test.model.anim; -import com.jme3.animation.*; +import com.jme3.anim.*; import com.jme3.app.ChaseCameraAppState; 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.light.DirectionalLight; import com.jme3.material.Material; import com.jme3.math.*; import com.jme3.scene.*; import com.jme3.scene.debug.custom.ArmatureDebugAppState; +import com.jme3.scene.shape.Cylinder; import com.jme3.util.TangentBinormalGenerator; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; + /** * Created by Nehon on 18/12/2017. */ @@ -25,6 +32,7 @@ public class TestArmature extends SimpleApplication { @Override public void simpleInitApp() { + setTimer(new EraseTimer()); renderManager.setSinglePassLightBatchSize(2); //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); viewPort.setBackgroundColor(ColorRGBA.DarkGray); @@ -34,20 +42,61 @@ public class TestArmature extends SimpleApplication { j2 = new Joint("Joint_2"); root.addChild(j1); j1.addChild(j2); - j1.setLocalTranslation(new Vector3f(0, 0.5f, 0)); - j1.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * 0.3f, Vector3f.UNIT_Z)); - j2.setLocalTranslation(new Vector3f(0, 0.2f, 0)); + root.setLocalTranslation(new Vector3f(0, 0, 0.5f)); + j1.setLocalTranslation(new Vector3f(0, 0.0f, -0.5f)); + j2.setLocalTranslation(new Vector3f(0, 0.0f, -0.2f)); Joint[] joints = new Joint[]{root, j1, j2}; - Armature armature = new Armature(joints); + final Armature armature = new Armature(joints); armature.setBindPose(); - ArmatureControl ac = new ArmatureControl(armature); + AnimClip clip = new AnimClip("anim"); + float[] times = new float[]{0, 2, 4}; + Quaternion[] rotations = new Quaternion[]{ + new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X), + new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X), + new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X) + }; + Vector3f[] translations = new Vector3f[]{ + new Vector3f(0, 0.2f, 0), + new Vector3f(0, 1.0f, 0), + new Vector3f(0, 0.2f, 0), + }; + Vector3f[] scales = new Vector3f[]{ + new Vector3f(1, 1, 1), + new Vector3f(2, 2, 2), + new Vector3f(1, 1, 1), + }; + Vector3f[] scales2 = new Vector3f[]{ + new Vector3f(1, 1, 1), + new Vector3f(0.5f, 0.5f, 0.5f), + new Vector3f(1, 1, 1), + }; + + JointTrack track1 = new JointTrack(j1, times, null, rotations, null); + JointTrack track2 = new JointTrack(j2, times, null, rotations, null); + clip.addTrack(track1); + clip.addTrack(track2); + + final AnimComposer composer = new AnimComposer(); + composer.addAnimClip(clip); + + SkinningControl ac = new SkinningControl(armature); + ac.setHardwareSkinningPreferred(false); Node node = new Node("Test Armature"); + rootNode.attachChild(node); + Geometry cylinder = new Geometry("cylinder", createMesh()); + Material m = new Material(assetManager, "Common/MatDefs/Misc/fakeLighting.j3md"); + m.setColor("Color", ColorRGBA.randomColor()); + cylinder.setMaterial(m); + node.attachChild(cylinder); + node.addControl(composer); node.addControl(ac); + composer.setCurrentAnimClip("anim"); + ArmatureDebugAppState debugAppState = new ArmatureDebugAppState(); debugAppState.addArmature(ac, true); stateManager.attach(debugAppState); @@ -71,6 +120,23 @@ public class TestArmature extends SimpleApplication { chaseCam.setMinDistance(0.01f); chaseCam.setZoomSpeed(0.01f); chaseCam.setDefaultVerticalRotation(0.3f); + + + inputManager.addMapping("bind", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed) { + play = false; + composer.reset(); + armature.resetToBindPose(); + + } else { + play = true; + composer.setCurrentAnimClip("anim"); + } + } + }, "bind"); } @@ -99,14 +165,70 @@ public class TestArmature extends SimpleApplication { }); } + private Mesh createMesh() { + Cylinder c = new Cylinder(30, 16, 0.1f, 1, true); + + ShortBuffer jointIndex = (ShortBuffer) VertexBuffer.createBuffer(VertexBuffer.Format.UnsignedShort, 4, c.getVertexCount()); + jointIndex.rewind(); + c.setMaxNumWeights(1); + FloatBuffer jointWeight = (FloatBuffer) VertexBuffer.createBuffer(VertexBuffer.Format.Float, 4, c.getVertexCount()); + jointWeight.rewind(); + VertexBuffer vb = c.getBuffer(VertexBuffer.Type.Position); + FloatBuffer fvb = (FloatBuffer) vb.getData(); + fvb.rewind(); + for (int i = 0; i < c.getVertexCount(); i++) { + fvb.get(); + fvb.get(); + float z = fvb.get(); + int index = 0; + if (z > 0) { + index = 0; + } else if (z > -0.2) { + index = 1; + } else { + index = 2; + } + jointIndex.put((short) index).put((short) 0).put((short) 0).put((short) 0); + jointWeight.put(1f).put(0f).put(0f).put(0f); + + } + c.setBuffer(VertexBuffer.Type.BoneIndex, 4, jointIndex); + c.setBuffer(VertexBuffer.Type.BoneWeight, 4, jointWeight); + + c.updateCounts(); + c.updateBound(); + //the mesh has some skinning let's create needed buffers for HW skinning + //creating empty buffers for HW skinning + //the buffers will be setup if ever used. + VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight); + VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex); + //setting usage to cpuOnly so that the buffer is not send empty to the GPU + indicesHW.setUsage(VertexBuffer.Usage.CpuOnly); + weightsHW.setUsage(VertexBuffer.Usage.CpuOnly); + c.setBuffer(weightsHW); + c.setBuffer(indicesHW); + c.generateBindPose(); + + c.prepareForAnim(false); + + return c; + } + + float time = 0; + boolean play = true; @Override public void simpleUpdate(float tpf) { - time += tpf; - float rot = FastMath.sin(time); - j1.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * rot, Vector3f.UNIT_Z)); - j2.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * rot, Vector3f.UNIT_Z)); + + +// if (play == false) { +// return; +// } +// time += tpf; +// float rot = FastMath.sin(time); +// j1.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * rot, Vector3f.UNIT_Z)); +// j2.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * rot, Vector3f.UNIT_Z)); } } From b5ad72b0e98c6a9e7d4c274380018defe0199045 Mon Sep 17 00:00:00 2001 From: Nehon Date: Sat, 23 Dec 2017 16:11:55 +0100 Subject: [PATCH 07/54] Joint now uses a matrix for transform accumulation --- .../src/main/java/com/jme3/anim/Joint.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/anim/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java index 3797039fe..c2c92693b 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Joint.java +++ b/jme3-core/src/main/java/com/jme3/anim/Joint.java @@ -33,7 +33,6 @@ public class Joint implements Savable, JmeCloneable { * Or relative to the model's origin for the root joint. */ private Transform localTransform = new Transform(); - /** * The base transform of the joint in local space. * Those transform are the joint's initial value. @@ -44,6 +43,7 @@ public class Joint implements Savable, JmeCloneable { * The transform of the joint in model space. Relative to the origin of the model. */ private Transform modelTransform = new Transform(); + private Matrix4f modelTransformMatrix = new Matrix4f(); /** * The matrix used to transform affected vertices position into the joint model space. @@ -78,10 +78,11 @@ public class Joint implements Savable, JmeCloneable { * model transform with this bones' local transform. */ public final void updateModelTransforms() { - modelTransform.set(localTransform); + localTransform.toTransformMatrix(modelTransformMatrix); if (parent != null) { - modelTransform.combineWithParent(parent.getModelTransform()); + parent.modelTransformMatrix.mult(modelTransformMatrix, modelTransformMatrix); } + modelTransform.fromTransformMatrix(modelTransformMatrix); updateAttachNode(); } @@ -130,21 +131,23 @@ public class Joint implements Savable, JmeCloneable { * @param outTransform */ void getOffsetTransform(Matrix4f outTransform) { - modelTransform.toTransformMatrix(outTransform).mult(inverseModelBindMatrix, outTransform); + outTransform.set(modelTransformMatrix).mult(inverseModelBindMatrix, outTransform); } protected void setBindPose() { //Note that the whole Armature must be updated before calling this method. - modelTransform.toTransformMatrix(inverseModelBindMatrix); + inverseModelBindMatrix.set(modelTransformMatrix); inverseModelBindMatrix.invertLocal(); baseLocalTransform.set(localTransform); } protected void resetToBindPose() { - localTransform.fromTransformMatrix(inverseModelBindMatrix.invert()); // local = modelBind + //just using modelTransformMatrix as a temp matrix here + modelTransformMatrix.set(inverseModelBindMatrix).invertLocal(); // model transform = model bind if (parent != null) { - localTransform.combineWithParent(parent.modelTransform.invert()); // local = local Bind + parent.modelTransformMatrix.invert().mult(modelTransformMatrix, modelTransformMatrix); } + localTransform.fromTransformMatrix(modelTransformMatrix); updateModelTransforms(); for (Joint child : children) { From ce170b8b531c59b6753d47154f4d1ee1df32ff68 Mon Sep 17 00:00:00 2001 From: Nehon Date: Sat, 23 Dec 2017 16:12:37 +0100 Subject: [PATCH 08/54] better Armature debugger --- .../jme3/anim/util/AnimMigrationUtils.java | 24 ++ .../main/java/com/jme3/math/MathUtils.java | 63 +++++ .../jme3/scene/debug/custom/ArmatureBone.java | 199 -------------- .../debug/custom/ArmatureDebugAppState.java | 91 ++++--- .../scene/debug/custom/ArmatureDebugger.java | 151 ++++------- .../jme3/scene/debug/custom/ArmatureNode.java | 245 ++++++++++++++++++ .../jme3/scene/debug/custom/BoneShape.java | 171 ------------ .../jme3/scene/debug/custom/JointShape.java | 79 ++++++ .../Common/MatDefs/Misc/Billboard.j3md | 78 ++++++ .../MatDefs/ShaderNodes/Common/Billboard.j3sn | 35 +++ .../ShaderNodes/Common/Billboard100.frag | 19 ++ .../ShaderNodes/Common/FixedScale.j3sn | 41 +++ .../ShaderNodes/Common/FixedScale100.frag | 8 + .../MatDefs/ShaderNodes/Common/texCoord.j3sn | 57 ++++ .../ShaderNodes/Common/texCoord100.frag | 2 + .../main/resources/Common/Textures/dot.png | Bin 0 -> 367 bytes .../jme3test/model/anim/TestArmature.java | 15 +- 17 files changed, 753 insertions(+), 525 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java delete mode 100644 jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java create mode 100644 jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java delete mode 100644 jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java create mode 100644 jme3-core/src/main/java/com/jme3/scene/debug/custom/JointShape.java create mode 100644 jme3-core/src/main/resources/Common/MatDefs/Misc/Billboard.j3md create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/Billboard.j3sn create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/Billboard100.frag create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale.j3sn create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale100.frag create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/texCoord.j3sn create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/texCoord100.frag create mode 100644 jme3-core/src/main/resources/Common/Textures/dot.png diff --git a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java new file mode 100644 index 000000000..f11a0c43c --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java @@ -0,0 +1,24 @@ +package com.jme3.anim.util; + +import com.jme3.animation.AnimControl; +import com.jme3.scene.SceneGraphVisitor; +import com.jme3.scene.Spatial; + +public class AnimMigrationUtils { + + public static Spatial migrate(Spatial source) { + //source.depthFirstTraversal(); + return source; + } + + private class AnimControlVisitor implements SceneGraphVisitor { + + @Override + public void visit(Spatial spatial) { + AnimControl control = spatial.getControl(AnimControl.class); + if (control != null) { + + } + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/math/MathUtils.java b/jme3-core/src/main/java/com/jme3/math/MathUtils.java index a27c266ce..234d5f61b 100644 --- a/jme3-core/src/main/java/com/jme3/math/MathUtils.java +++ b/jme3-core/src/main/java/com/jme3/math/MathUtils.java @@ -1,5 +1,7 @@ package com.jme3.math; +import com.jme3.util.TempVars; + /** * Created by Nehon on 23/04/2017. */ @@ -162,4 +164,65 @@ public class MathUtils { } + public static float raySegmentShortestDistance(Ray ray, Vector3f segStart, Vector3f segEnd) { + // Algorithm is ported from the C algorithm of + // Paul Bourke at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline3d/ + TempVars vars = TempVars.get(); + Vector3f resultSegmentPoint1 = vars.vect1; + Vector3f resultSegmentPoint2 = vars.vect2; + + Vector3f p1 = segStart; + Vector3f p2 = segEnd; + Vector3f p3 = ray.origin; + Vector3f p4 = vars.vect3.set(ray.getDirection()).multLocal(Math.min(ray.getLimit(), 1000)).addLocal(ray.getOrigin()); + Vector3f p13 = vars.vect4.set(p1).subtractLocal(p3); + Vector3f p43 = vars.vect5.set(p4).subtractLocal(p3); + + if (p43.lengthSquared() < 0.0001) { + vars.release(); + return -1; + } + Vector3f p21 = vars.vect6.set(p2).subtractLocal(p1); + if (p21.lengthSquared() < 0.0001) { + vars.release(); + return -1; + } + + double d1343 = p13.x * (double) p43.x + (double) p13.y * p43.y + (double) p13.z * p43.z; + double d4321 = p43.x * (double) p21.x + (double) p43.y * p21.y + (double) p43.z * p21.z; + double d1321 = p13.x * (double) p21.x + (double) p13.y * p21.y + (double) p13.z * p21.z; + double d4343 = p43.x * (double) p43.x + (double) p43.y * p43.y + (double) p43.z * p43.z; + double d2121 = p21.x * (double) p21.x + (double) p21.y * p21.y + (double) p21.z * p21.z; + + double denom = d2121 * d4343 - d4321 * d4321; + if (Math.abs(denom) < 0.0001) { + vars.release(); + return -1; + } + double numer = d1343 * d4321 - d1321 * d4343; + + double mua = numer / denom; + double mub = (d1343 + d4321 * (mua)) / d4343; + + resultSegmentPoint1.x = (float) (p1.x + mua * p21.x); + resultSegmentPoint1.y = (float) (p1.y + mua * p21.y); + resultSegmentPoint1.z = (float) (p1.z + mua * p21.z); + resultSegmentPoint2.x = (float) (p3.x + mub * p43.x); + resultSegmentPoint2.y = (float) (p3.y + mub * p43.y); + resultSegmentPoint2.z = (float) (p3.z + mub * p43.z); + + //check if result 1 is in the segment section. + float startToPoint = vars.vect3.set(resultSegmentPoint1).subtractLocal(segStart).lengthSquared(); + float endToPoint = vars.vect3.set(resultSegmentPoint1).subtractLocal(segEnd).lengthSquared(); + float segLength = vars.vect3.set(segEnd).subtractLocal(segStart).lengthSquared(); + if (startToPoint > segLength || endToPoint > segLength) { + vars.release(); + return -1; + } + + float length = resultSegmentPoint1.subtractLocal(resultSegmentPoint2).length(); + vars.release(); + return length; + } + } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java deleted file mode 100644 index 3e3149f59..000000000 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureBone.java +++ /dev/null @@ -1,199 +0,0 @@ -package com.jme3.scene.debug.custom; - -/* - * 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. - */ - -import com.jme3.anim.Armature; -import com.jme3.anim.Joint; -import com.jme3.bounding.*; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; -import com.jme3.scene.*; -import com.jme3.scene.shape.Sphere; - -import java.nio.FloatBuffer; -import java.util.HashMap; -import java.util.Map; - -import static com.jme3.util.BufferUtils.createFloatBuffer; - -/** - * The class that displays either wires between the bones' heads if no length - * data is supplied and full bones' shapes otherwise. - */ -public class ArmatureBone extends Node { - - /** - * The armature to be displayed. - */ - private Armature armature; - /** - * The map between the bone index and its length. - */ - private Map jointNode = new HashMap<>(); - private Map nodeJoint = new HashMap<>(); - private Node selectedNode = null; - private boolean guessJointsOrientation = false; - - /** - * Creates a wire with bone lengths data. If the data is supplied then the - * wires will show each full bone (from head to tail). - * - * @param armature the armature that will be shown - * @param boneLengths a map between the bone's index and the bone's length - */ - public ArmatureBone(Armature armature, Map boneLengths, boolean guessJointsOrientation) { - this.armature = armature; - this.guessJointsOrientation = guessJointsOrientation; - - BoneShape boneShape = new BoneShape(); - Sphere jointShape = new Sphere(16, 16, 0.05f); - jointShape.setBuffer(VertexBuffer.Type.Color, 4, createFloatBuffer(jointShape.getVertexCount() * 4)); - FloatBuffer cb = jointShape.getFloatBuffer(VertexBuffer.Type.Color); - - cb.rewind(); - for (int i = 0; i < jointShape.getVertexCount(); i++) { - cb.put(0.05f).put(0.05f).put(0.05f).put(1f); - } - - for (Joint joint : armature.getRoots()) { - createSkeletonGeoms(joint, boneShape, jointShape, boneLengths, armature, this, guessJointsOrientation); - } - this.updateModelBound(); - - - Sphere originShape = new Sphere(16, 16, 0.02f); - originShape.setBuffer(VertexBuffer.Type.Color, 4, createFloatBuffer(originShape.getVertexCount() * 4)); - cb = originShape.getFloatBuffer(VertexBuffer.Type.Color); - cb.rewind(); - for (int i = 0; i < jointShape.getVertexCount(); i++) { - cb.put(0.4f).put(0.4f).put(0.05f).put(1f); - } - - Geometry origin = new Geometry("origin", originShape); - BoundingVolume bv = this.getWorldBound(); - float scale = 1; - if (bv.getType() == BoundingVolume.Type.AABB) { - BoundingBox bb = (BoundingBox) bv; - scale = (bb.getXExtent() + bb.getYExtent() + bb.getZExtent()) / 3f; - } else if (bv.getType() == BoundingVolume.Type.Sphere) { - BoundingSphere bs = (BoundingSphere) bv; - scale = bs.getRadius(); - } - origin.scale(scale); - attachChild(origin); - } - - protected final void createSkeletonGeoms(Joint joint, Mesh boneShape, Mesh jointShape, Map boneLengths, Armature armature, Node parent, boolean guessBonesOrientation) { - - Node n = new Node(joint.getName() + "Node"); - Geometry bGeom = new Geometry(joint.getName() + "Bone", boneShape); - Geometry jGeom = new Geometry(joint.getName() + "Joint", jointShape); - n.setLocalTransform(joint.getLocalTransform()); - - float boneLength = boneLengths.get(armature.getJointIndex(joint)); - - if (guessBonesOrientation) { - //One child only, the bone direction is from the parent joint to the child joint. - if (joint.getChildren().size() == 1) { - Vector3f v = joint.getChildren().get(0).getLocalTranslation(); - Quaternion q = new Quaternion(); - q.lookAt(v, Vector3f.UNIT_Z); - bGeom.setLocalRotation(q); - } - //no child, the bone has the same direction as the parent bone. - if (joint.getChildren().isEmpty()) { - //no parent, let's use the bind orientation of the bone - Spatial s = parent.getChild(0); - if (s != null) { - bGeom.setLocalRotation(s.getLocalRotation()); - } - } - } - - float boneScale = boneLength * 0.8f; - float scale = boneScale / 8f; - bGeom.setLocalScale(new Vector3f(scale, scale, boneScale)); - Vector3f offset = new Vector3f(0, 0, boneLength * 0.1f); - bGeom.getLocalRotation().multLocal(offset); - bGeom.setLocalTranslation(offset); - jGeom.setLocalScale(boneLength); - - if (joint.getChildren().size() <= 1) { - n.attachChild(bGeom); - } - n.attachChild(jGeom); - - jointNode.put(joint, n); - nodeJoint.put(n, joint); - parent.attachChild(n); - for (Joint child : joint.getChildren()) { - createSkeletonGeoms(child, boneShape, jointShape, boneLengths, armature, n, guessBonesOrientation); - } - } - - protected Joint select(Geometry g) { - Node parentNode = g.getParent(); - - if (parent != null) { - Joint j = nodeJoint.get(parentNode); - if (j != null) { - selectedNode = parentNode; - } - return j; - } - return null; - } - - protected Node getSelectedNode() { - return selectedNode; - } - - - protected final void updateSkeletonGeoms(Joint joint) { - Node n = jointNode.get(joint); - n.setLocalTransform(joint.getLocalTransform()); - - for (Joint child : joint.getChildren()) { - updateSkeletonGeoms(child); - } - } - - /** - * The method updates the geometry according to the positions of the bones. - */ - public void updateGeometry() { - for (Joint joint : armature.getRoots()) { - updateSkeletonGeoms(joint); - } - } -} diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java index 06661eff7..439316a95 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java @@ -8,12 +8,12 @@ import com.jme3.anim.*; import com.jme3.app.Application; import com.jme3.app.state.AbstractAppState; import com.jme3.app.state.AppStateManager; +import com.jme3.collision.CollisionResults; import com.jme3.input.MouseInput; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.MouseButtonTrigger; import com.jme3.light.DirectionalLight; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector3f; +import com.jme3.math.*; import com.jme3.renderer.ViewPort; import com.jme3.scene.*; @@ -28,7 +28,6 @@ public class ArmatureDebugAppState extends AbstractAppState { private Map armatures = new HashMap<>(); private Map selectedBones = new HashMap<>(); private Application app; - @Override public void initialize(AppStateManager stateManager, Application app) { ViewPort vp = app.getRenderManager().createMainView("debug", app.getCamera()); @@ -46,7 +45,6 @@ public class ArmatureDebugAppState extends AbstractAppState { debugNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal())); debugNode.addLight(new DirectionalLight(new Vector3f(1f, 1f, 1f).normalizeLocal(), new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f))); - } @Override @@ -55,15 +53,15 @@ public class ArmatureDebugAppState extends AbstractAppState { debugNode.updateGeometricState(); } - public ArmatureDebugger addArmature(SkinningControl skinningControl, boolean guessJointsOrientation) { + public ArmatureDebugger addArmature(SkinningControl skinningControl) { Armature armature = skinningControl.getArmature(); Spatial forSpatial = skinningControl.getSpatial(); - return addArmature(armature, forSpatial, guessJointsOrientation); + return addArmature(armature, forSpatial); } - public ArmatureDebugger addArmature(Armature armature, Spatial forSpatial, boolean guessJointsOrientation) { + public ArmatureDebugger addArmature(Armature armature, Spatial forSpatial) { - ArmatureDebugger ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature, guessJointsOrientation); + ArmatureDebugger ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature); ad.setLocalTransform(forSpatial.getWorldTransform()); if (forSpatial instanceof Node) { List geoms = new ArrayList<>(); @@ -98,45 +96,44 @@ public class ArmatureDebugAppState extends AbstractAppState { */ private ActionListener actionListener = new ActionListener() { public void onAction(String name, boolean isPressed, float tpf) { - //if (name.equals("shoot") && isPressed) { -// CollisionResults results = new CollisionResults(); -// Vector2f click2d = app.getInputManager().getCursorPosition(); -// Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); -// Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); -// Ray ray = new Ray(click3d, dir); -// -// debugNode.collideWith(ray, results); -// -// if (results.size() > 0) { -// // The closest result is the target that the player picked: -// Geometry target = results.getClosestCollision().getGeometry(); -// for (ArmatureDebugger skeleton : armatures.values()) { -// Joint selectedBone = skeleton.select(target); -// if (selectedBone != null) { -// selectedBones.put(skeleton.getArmature(), selectedBone); -// System.err.println("-----------------------"); -// System.err.println("Selected Bone : " + selectedBone.getName() + " in skeleton " + skeleton.getName()); -// System.err.println("Root Bone : " + (selectedBone.getParent() == null)); -// System.err.println("-----------------------"); -// System.err.println("Bind translation: " + selectedBone.getBindPosition()); -// System.err.println("Bind rotation: " + selectedBone.getBindRotation()); -// System.err.println("Bind scale: " + selectedBone.getBindScale()); -// System.err.println("---"); -// System.err.println("Local translation: " + selectedBone.getLocalPosition()); -// System.err.println("Local rotation: " + selectedBone.getLocalRotation()); -// System.err.println("Local scale: " + selectedBone.getLocalScale()); -// System.err.println("---"); -// System.err.println("Model translation: " + selectedBone.getModelSpacePosition()); -// System.err.println("Model rotation: " + selectedBone.getModelSpaceRotation()); -// System.err.println("Model scale: " + selectedBone.getModelSpaceScale()); -// System.err.println("---"); -// System.err.println("Bind inverse Transform: "); -// System.err.println(selectedBone.getBindInverseTransform()); -// return; -// } -// } -// } -// } + if (name.equals("shoot") && isPressed) { + CollisionResults results = new CollisionResults(); + Vector2f click2d = app.getInputManager().getCursorPosition(); + Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); + Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); + Ray ray = new Ray(click3d, dir); + + debugNode.collideWith(ray, results); + if (results.size() == 0) { + for (ArmatureDebugger ad : armatures.values()) { + ad.select(null); + } + return; + } + // The closest result is the target that the player picked: + Geometry target = results.getClosestCollision().getGeometry(); + for (ArmatureDebugger ad : armatures.values()) { + Joint selectedjoint = ad.select(target); + if (selectedjoint != null) { + selectedBones.put(ad.getArmature(), selectedjoint); + System.err.println("-----------------------"); + System.err.println("Selected Joint : " + selectedjoint.getName() + " in armature " + ad.getName()); + System.err.println("Root Bone : " + (selectedjoint.getParent() == null)); + System.err.println("-----------------------"); + System.err.println("Local translation: " + selectedjoint.getLocalTranslation()); + System.err.println("Local rotation: " + selectedjoint.getLocalRotation()); + System.err.println("Local scale: " + selectedjoint.getLocalScale()); + System.err.println("---"); + System.err.println("Model translation: " + selectedjoint.getModelTransform().getTranslation()); + System.err.println("Model rotation: " + selectedjoint.getModelTransform().getRotation()); + System.err.println("Model scale: " + selectedjoint.getModelTransform().getScale()); + System.err.println("---"); + System.err.println("Bind inverse Transform: "); + System.err.println(selectedjoint.getInverseModelBindMatrix()); + return; + } + } + } } }; diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java index 8d068c185..43a4e41d7 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java @@ -37,31 +37,39 @@ import com.jme3.anim.Joint; import com.jme3.animation.Bone; import com.jme3.asset.AssetManager; import com.jme3.material.Material; -import com.jme3.math.ColorRGBA; -import com.jme3.scene.*; +import com.jme3.material.RenderState; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.texture.Texture; -import java.nio.FloatBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.List; /** * The class that creates a mesh to display how bones behave. If it is supplied * with the bones' lengths it will show exactly how the bones look like on the * scene. If not then only connections between each bone heads will be shown. */ -public class ArmatureDebugger extends BatchNode { +public class ArmatureDebugger extends Node { /** * The lines of the bones or the wires between their heads. */ - private ArmatureBone bones; + private ArmatureNode armatureNode; private Armature armature; + + Node joints; + Node outlines; + Node wires; + /** * The dotted lines between a bone's tail and the had of its children. Not * available if the length data was not provided. */ private ArmatureInterJointsWire interJointWires; - private Geometry wires; + //private Geometry wires; private List selectedJoints = new ArrayList(); public ArmatureDebugger() { @@ -75,110 +83,74 @@ public class ArmatureDebugger extends BatchNode { * @param name the name of the debugger's node * @param armature the armature that will be shown */ - public ArmatureDebugger(String name, Armature armature, boolean guessJointsOrientation) { + public ArmatureDebugger(String name, Armature armature) { super(name); this.armature = armature; -// armature.reset(); armature.update(); - //Joints have no length we want to display the as bones so we compute their length - Map bonesLength = new HashMap(); - for (Joint joint : armature.getRoots()) { - computeLength(joint, bonesLength, armature); - } - bones = new ArmatureBone(armature, bonesLength, guessJointsOrientation); + joints = new Node("joints"); + outlines = new Node("outlines"); + wires = new Node("bones"); + this.attachChild(joints); + this.attachChild(outlines); + this.attachChild(wires); - this.attachChild(bones); + armatureNode = new ArmatureNode(armature, joints, wires, outlines); - interJointWires = new ArmatureInterJointsWire(armature, bonesLength, guessJointsOrientation); - wires = new Geometry(name + "_interwires", interJointWires); + this.attachChild(armatureNode); + + //interJointWires = new ArmatureInterJointsWire(armature, bonesLength, guessJointsOrientation); + //wires = new Geometry(name + "_interwires", interJointWires); // this.attachChild(wires); } protected void initialize(AssetManager assetManager) { - Material mat = new Material(assetManager, "Common/MatDefs/Misc/fakeLighting.j3md"); - mat.setColor("Color", new ColorRGBA(0.2f, 0.2f, 0.2f, 1)); - setMaterial(mat); - Material matWires = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - matWires.setColor("Color", ColorRGBA.Black); + matWires.setBoolean("VertexColor", true); + matWires.getAdditionalRenderState().setLineWidth(3); wires.setMaterial(matWires); - //wires.setQueueBucket(RenderQueue.Bucket.Transparent); -// Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); -// mat2.setBoolean("VertexColor", true); -// bones.setMaterial(mat2); -// batch(); - } - @Override - public final void setMaterial(Material material) { - if (batches.isEmpty()) { - for (int i = 0; i < children.size(); i++) { - children.get(i).setMaterial(material); - } - } else { - super.setMaterial(material); - } + Material matOutline = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + matOutline.setBoolean("VertexColor", true); + //matOutline.setColor("Color", ColorRGBA.White); + matOutline.getAdditionalRenderState().setLineWidth(5); + outlines.setMaterial(matOutline); + + Material matJoints = new Material(assetManager, "Common/MatDefs/Misc/Billboard.j3md"); + Texture t = assetManager.loadTexture("Common/Textures/dot.png"); +// matJoints.setBoolean("VertexColor", true); +// matJoints.setTexture("ColorMap", t); + matJoints.setTexture("Texture", t); + matJoints.getAdditionalRenderState().setDepthTest(false); + matJoints.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + joints.setQueueBucket(RenderQueue.Bucket.Translucent); + joints.setMaterial(matJoints); + } public Armature getArmature() { return armature; } - - private void computeLength(Joint joint, Map jointsLength, Armature armature) { - if (joint.getChildren().isEmpty()) { - if (joint.getParent() != null) { - jointsLength.put(armature.getJointIndex(joint), jointsLength.get(armature.getJointIndex(joint.getParent())) * 0.75f); - } else { - jointsLength.put(armature.getJointIndex(joint), 0.1f); - } - } else { - float length = Float.MAX_VALUE; - for (Joint child : joint.getChildren()) { - float len = joint.getModelTransform().getTranslation().subtract(child.getModelTransform().getTranslation()).length(); - if (len < length) { - length = len; - } - } - jointsLength.put(armature.getJointIndex(joint), length); - for (Joint child : joint.getChildren()) { - computeLength(child, jointsLength, armature); - } - } - } - @Override public void updateLogicalState(float tpf) { super.updateLogicalState(tpf); - bones.updateGeometry(); + armatureNode.updateGeometry(); if (interJointWires != null) { // interJointWires.updateGeometry(); } } - ColorRGBA selectedColor = ColorRGBA.Orange; - ColorRGBA baseColor = new ColorRGBA(0.05f, 0.05f, 0.05f, 1f); - protected Joint select(Geometry g) { - Node oldNode = bones.getSelectedNode(); - Joint b = bones.select(g); - if (b == null) { - return null; - } - if (oldNode != null) { - markSelected(oldNode, false); - } - markSelected(bones.getSelectedNode(), true); - return b; + return armatureNode.select(g); } /** * @return the armature wires */ - public ArmatureBone getBoneShapes() { - return bones; + public ArmatureNode getBoneShapes() { + return armatureNode; } /** @@ -187,29 +159,4 @@ public class ArmatureDebugger extends BatchNode { public ArmatureInterJointsWire getInterJointWires() { return interJointWires; } - - protected void markSelected(Node n, boolean selected) { - ColorRGBA c = baseColor; - if (selected) { - c = selectedColor; - } - for (Spatial spatial : n.getChildren()) { - if (spatial instanceof Geometry) { - Geometry geom = (Geometry) spatial; - - Geometry batch = (Geometry) getChild(getName() + "-batch0"); - VertexBuffer vb = batch.getMesh().getBuffer(VertexBuffer.Type.Color); - FloatBuffer color = (FloatBuffer) vb.getData(); - // System.err.println(getName() + "." + geom.getName() + " index " + getGeometryStartIndex(geom) * 4 + "/" + color.limit()); - - color.position(getGeometryStartIndex(geom) * 4); - - for (int i = 0; i < geom.getVertexCount(); i++) { - color.put(c.r).put(c.g).put(c.b).put(c.a); - } - color.rewind(); - vb.updateData(color); - } - } - } } \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java new file mode 100644 index 000000000..2b717adfd --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java @@ -0,0 +1,245 @@ +package com.jme3.scene.debug.custom; + +/* + * 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. + */ + +import com.jme3.anim.Armature; +import com.jme3.anim.Joint; +import com.jme3.collision.*; +import com.jme3.math.*; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.*; +import com.jme3.scene.shape.Line; + +import java.nio.FloatBuffer; +import java.util.HashMap; +import java.util.Map; + +/** + * The class that displays either wires between the bones' heads if no length + * data is supplied and full bones' shapes otherwise. + */ +public class ArmatureNode extends Node { + + /** + * The armature to be displayed. + */ + private Armature armature; + /** + * The map between the bone index and its length. + */ + private Map jointToGeoms = new HashMap<>(); + private Map geomToJoint = new HashMap<>(); + private Joint selectedJoint = null; + private Vector3f tmpStart = new Vector3f(); + private Vector3f tmpEnd = new Vector3f(); + ColorRGBA selectedColor = ColorRGBA.Orange; + ColorRGBA selectedColorJ = ColorRGBA.Yellow; + ;//new ColorRGBA(0.2f, 1f, 1.0f, 1.0f); + ColorRGBA baseColor = new ColorRGBA(0.05f, 0.05f, 0.05f, 1f); + + + /** + * Creates a wire with bone lengths data. If the data is supplied then the + * wires will show each full bone (from head to tail). + * + * @param armature the armature that will be shown + */ + public ArmatureNode(Armature armature, Node joints, Node wires, Node outlines) { + this.armature = armature; + + for (Joint joint : armature.getRoots()) { + createSkeletonGeoms(joint, joints, wires, outlines); + } + this.updateModelBound(); + + } + + protected final void createSkeletonGeoms(Joint joint, Node joints, Node wires, Node outlines) { + Vector3f start = joint.getModelTransform().getTranslation().clone(); + Vector3f end = null; + + //One child only, the bone direction is from the parent joint to the child joint. + if (joint.getChildren().size() == 1) { + end = joint.getChildren().get(0).getModelTransform().getTranslation().clone(); + } + + Geometry jGeom = new Geometry(joint.getName() + "Joint", new JointShape()); + jGeom.setLocalTranslation(start); + joints.attachChild(jGeom); + Geometry bGeom = null; + Geometry bGeomO = null; + if (end != null) { + bGeom = new Geometry(joint.getName() + "Bone", new Line(start, end)); + setColor(bGeom, baseColor); + geomToJoint.put(bGeom, joint); + bGeomO = new Geometry(joint.getName() + "BoneOutline", new Line(start, end)); + setColor(bGeomO, ColorRGBA.White); + bGeom.setUserData("start", wires.getWorldTransform().transformVector(start, start)); + bGeom.setUserData("end", wires.getWorldTransform().transformVector(end, end)); + bGeom.setQueueBucket(RenderQueue.Bucket.Transparent); + wires.attachChild(bGeom); + outlines.attachChild(bGeomO); + } + + jointToGeoms.put(joint, new Geometry[]{jGeom, bGeom, bGeomO}); + + for (Joint child : joint.getChildren()) { + createSkeletonGeoms(child, joints, wires, outlines); + } + } + + protected Joint select(Geometry g) { + if (g == null) { + resetSelection(); + return null; + } + Joint j = geomToJoint.get(g); + if (j != null) { + if (selectedJoint == j) { + return null; + } + resetSelection(); + selectedJoint = j; + Geometry[] geomArray = jointToGeoms.get(selectedJoint); + setColor(geomArray[0], selectedColorJ); + setColor(geomArray[1], selectedColor); + setColor(geomArray[2], baseColor); + return j; + } + return null; + } + + private void resetSelection() { + if (selectedJoint == null) { + return; + } + Geometry[] geoms = jointToGeoms.get(selectedJoint); + setColor(geoms[0], ColorRGBA.White); + setColor(geoms[1], baseColor); + setColor(geoms[2], ColorRGBA.White); + selectedJoint = null; + } + + protected Joint getSelectedJoint() { + return selectedJoint; + } + + + protected final void updateSkeletonGeoms(Joint joint) { + Geometry[] geoms = jointToGeoms.get(joint); + if (geoms != null) { + Geometry jGeom = geoms[0]; + jGeom.setLocalTranslation(joint.getModelTransform().getTranslation()); + Geometry bGeom = geoms[1]; + if (bGeom != null) { + tmpStart.set(joint.getModelTransform().getTranslation()); + boolean hasEnd = false; + if (joint.getChildren().size() == 1) { + tmpEnd.set(joint.getChildren().get(0).getModelTransform().getTranslation()); + hasEnd = true; + } + if (hasEnd) { + updateBoneMesh(bGeom); + Geometry bGeomO = geoms[2]; + updateBoneMesh(bGeomO); + Vector3f start = bGeom.getUserData("start"); + Vector3f end = bGeom.getUserData("end"); + bGeom.setUserData("start", bGeom.getParent().getWorldTransform().transformVector(tmpStart, start)); + bGeom.setUserData("end", bGeom.getParent().getWorldTransform().transformVector(tmpEnd, end)); + } + } + } + + for (Joint child : joint.getChildren()) { + updateSkeletonGeoms(child); + } + } + + @Override + public int collideWith(Collidable other, CollisionResults results) { + if (!(other instanceof Ray)) { + return 0; + } + int nbCol = 0; + for (Geometry g : geomToJoint.keySet()) { + float len = MathUtils.raySegmentShortestDistance((Ray) other, (Vector3f) g.getUserData("start"), (Vector3f) g.getUserData("end")); + if (len > 0 && len < 0.1f) { + CollisionResult res = new CollisionResult(); + res.setGeometry(g); + results.addCollision(res); + nbCol++; + } + } + return nbCol; + } + + private void updateBoneMesh(Geometry bGeom) { + VertexBuffer pos = bGeom.getMesh().getBuffer(VertexBuffer.Type.Position); + FloatBuffer fb = (FloatBuffer) pos.getData(); + fb.rewind(); + fb.put(new float[]{tmpStart.x, tmpStart.y, tmpStart.z, + tmpEnd.x, tmpEnd.y, tmpEnd.z,}); + pos.updateData(fb); + + bGeom.updateModelBound(); + } + + private void setColor(Geometry g, ColorRGBA color) { + float[] colors = new float[g.getMesh().getVertexCount() * 4]; + for (int i = 0; i < g.getMesh().getVertexCount() * 4; i += 4) { + colors[i] = color.r; + colors[i + 1] = color.g; + colors[i + 2] = color.b; + colors[i + 3] = color.a; + } + VertexBuffer colorBuff = g.getMesh().getBuffer(VertexBuffer.Type.Color); + if (colorBuff == null) { + g.getMesh().setBuffer(VertexBuffer.Type.Color, 4, colors); + } else { + FloatBuffer cBuff = (FloatBuffer) colorBuff.getData(); + cBuff.rewind(); + cBuff.put(colors); + colorBuff.updateData(cBuff); + } + } + + /** + * The method updates the geometry according to the positions of the bones. + */ + public void updateGeometry() { + armature.update(); + for (Joint joint : armature.getRoots()) { + updateSkeletonGeoms(joint); + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java deleted file mode 100644 index 702ec5613..000000000 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (c) 2009-2018 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. - */ -// $Id: Cylinder.java 4131 2009-03-19 20:15:28Z blaine.dev $ -package com.jme3.scene.debug.custom; - -import com.jme3.math.*; -import com.jme3.scene.VertexBuffer.Type; -import com.jme3.scene.shape.AbstractBox; -import com.jme3.util.BufferUtils; - -import java.nio.FloatBuffer; - -/** - * A simple cylinder, defined by its height and radius. - * (Ported to jME3) - * - * @author Mark Powell - * @version $Revision: 4131 $, $Date: 2009-03-19 16:15:28 -0400 (Thu, 19 Mar 2009) $ - */ -public class BoneShape extends AbstractBox { - - private static Vector3f topN = new Vector3f(0, 1, 0); - private static Vector3f botN = new Vector3f(0, -1, 0); - private static Vector3f rigN = new Vector3f(1, 0, 0); - private static Vector3f lefN = new Vector3f(-1, 0, 0); - - static { - Quaternion q = new Quaternion().fromAngleAxis(-FastMath.PI / 16f, Vector3f.UNIT_X); - q.multLocal(topN); - q.inverseLocal(); - q.multLocal(botN); - q = new Quaternion().fromAngleAxis(FastMath.PI / 16f, Vector3f.UNIT_Y); - q.multLocal(rigN); - q.inverseLocal(); - q.multLocal(lefN); - } - - private static final short[] GEOMETRY_INDICES_DATA = { - 2, 1, 0, 3, 2, 0, // back - 6, 5, 4, 7, 6, 4, // right - 10, 9, 8, 11, 10, 8, // front - 14, 13, 12, 15, 14, 12, // left - 18, 17, 16, 19, 18, 16, // top - 22, 21, 20, 23, 22, 20 // bottom - }; - - private static final float[] GEOMETRY_NORMALS_DATA = { - 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, // back - rigN.x, rigN.y, rigN.z, rigN.x, rigN.y, rigN.z, rigN.x, rigN.y, rigN.z, rigN.x, rigN.y, rigN.z, // right - 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // front - lefN.x, lefN.y, lefN.z, lefN.x, lefN.y, lefN.z, lefN.x, lefN.y, lefN.z, lefN.x, lefN.y, lefN.z, // left - topN.x, topN.y, topN.z, topN.x, topN.y, topN.z, topN.x, topN.y, topN.z, topN.x, topN.y, topN.z, // top - botN.x, botN.y, botN.z, botN.x, botN.y, botN.z, botN.x, botN.y, botN.z, botN.x, botN.y, botN.z // bottom - }; - - private static final float[] GEOMETRY_TEXTURE_DATA = { - 1, 0, 0, 0, 0, 1, 1, 1, // back - 1, 0, 0, 0, 0, 1, 1, 1, // right - 1, 0, 0, 0, 0, 1, 1, 1, // front - 1, 0, 0, 0, 0, 1, 1, 1, // left - 1, 0, 0, 0, 0, 1, 1, 1, // top - 1, 0, 0, 0, 0, 1, 1, 1 // bottom - }; - - private static final float[] GEOMETRY_POSITION_DATA = { - -0.5f, -0.5f, 0, 0.5f, -0.5f, 0, 0.5f, 0.5f, 0, -0.5f, 0.5f, 0, //back - 0.5f, -0.5f, 0, 0.25f, -0.25f, 1, 0.25f, 0.25f, 1, 0.5f, 0.5f, 0, //right - 0.25f, -0.25f, 1, -0.25f, -0.25f, 1, -0.25f, 0.25f, 1, 0.25f, 0.25f, 1, //front - -0.25f, -0.25f, 1, -0.5f, -0.5f, 0, -0.5f, 0.5f, 0, -0.25f, 0.25f, 1, //left - 0.5f, 0.5f, 0, 0.25f, 0.25f, 1, -0.25f, 0.25f, 1, -0.5f, 0.5f, 0, // top - -0.5f, -0.5f, 0, -0.25f, -0.25f, 1, 0.25f, -0.25f, 1, 0.5f, -0.5f, 0 // bottom - }; - - //0,1,2,3 - //1,4,6,2 - //4,5,7,6 - //5,0,3,7, - //2,6,7,3 - //0,5,4,1 - - -// v[0].x, v[0].y, v[0].z, v[1].x, v[1].y, v[1].z, v[2].x, v[2].y, v[2].z, v[3].x, v[3].y, v[3].z, // back -// v[1].x, v[1].y, v[1].z, v[4].x, v[4].y, v[4].z, v[6].x, v[6].y, v[6].z, v[2].x, v[2].y, v[2].z, // right -// v[4].x, v[4].y, v[4].z, v[5].x, v[5].y, v[5].z, v[7].x, v[7].y, v[7].z, v[6].x, v[6].y, v[6].z, // front -// v[5].x, v[5].y, v[5].z, v[0].x, v[0].y, v[0].z, v[3].x, v[3].y, v[3].z, v[7].x, v[7].y, v[7].z, // left -// v[2].x, v[2].y, v[2].z, v[6].x, v[6].y, v[6].z, v[7].x, v[7].y, v[7].z, v[3].x, v[3].y, v[3].z, // top -// v[0].x, v[0].y, v[0].z, v[5].x, v[5].y, v[5].z, v[4].x, v[4].y, v[4].z, v[1].x, v[1].y, v[1].z // bottom - - /** - * Creates a new box. - *

- * The box has a center of 0,0,0 and extends in the out from the center by - * the given amount in each direction. So, for example, a box - * with extent of 0.5 would be the unit cube. - * - * @param x the size of the box along the x axis, in both directions. - * @param y the size of the box along the y axis, in both directions. - * @param z the size of the box along the z axis, in both directions. - */ - public BoneShape() { - super(); - updateGeometry(); - } - - /** - * Creates a clone of this box. - *

- * The cloned box will have '_clone' appended to it's name, but all other - * properties will be the same as this box. - */ - @Override - public BoneShape clone() { - return new BoneShape(); - } - - protected void doUpdateGeometryIndices() { - if (getBuffer(Type.Index) == null) { - setBuffer(Type.Index, 3, BufferUtils.createShortBuffer(GEOMETRY_INDICES_DATA)); - } - } - - protected void doUpdateGeometryNormals() { - if (getBuffer(Type.Normal) == null) { - setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(GEOMETRY_NORMALS_DATA)); - } - } - - protected void doUpdateGeometryTextures() { - if (getBuffer(Type.TexCoord) == null) { - setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(GEOMETRY_TEXTURE_DATA)); - } - } - - protected void doUpdateGeometryVertices() { - FloatBuffer fpb = BufferUtils.createVector3Buffer(24); - fpb.put(GEOMETRY_POSITION_DATA); - setBuffer(Type.Position, 3, fpb); - updateBound(); - } - - -} diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/JointShape.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/JointShape.java new file mode 100644 index 000000000..b854095b9 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/JointShape.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2009-2010 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.debug.custom; + +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer.Type; + + +public class JointShape extends Mesh { + + + /** + * Serialization only. Do not use. + */ + public JointShape() { + float width = 1; + float height = 1; + setBuffer(Type.Position, 3, new float[]{-width * 0.5f, -width * 0.5f, 0, + width * 0.5f, -width * 0.5f, 0, + width * 0.5f, height * 0.5f, 0, + -width * 0.5f, height * 0.5f, 0 + }); + + + setBuffer(Type.TexCoord, 2, new float[]{0, 0, + 1, 0, + 1, 1, + 0, 1}); + + setBuffer(Type.Normal, 3, new float[]{0, 0, 1, + 0, 0, 1, + 0, 0, 1, + 0, 0, 1}); + + setBuffer(Type.Color, 4, new float[]{1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1}); + + setBuffer(Type.Index, 3, new short[]{0, 1, 2, + 0, 2, 3}); + + + updateBound(); + setStatic(); + } + + +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/Billboard.j3md b/jme3-core/src/main/resources/Common/MatDefs/Misc/Billboard.j3md new file mode 100644 index 000000000..f97b3c736 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Billboard.j3md @@ -0,0 +1,78 @@ +MaterialDef Billboard { + MaterialParameters { + Float SpriteHeight : 10 + Texture2D Texture + } + + Technique { + WorldParameters { + WorldViewMatrix + ProjectionMatrix + WorldMatrix + CameraDirection + ViewPort + CameraPosition + } + + VertexShaderNodes { + ShaderNode TexCoord { + Definition: AttributeToVarying: Common/MatDefs/ShaderNodes/Basic/AttributeToVarying.j3sn + InputMappings { + vec2Variable = Attr.inTexCoord + vec4Variable = Attr.inColor + } + OutputMappings { + } + } + ShaderNode FixedScale { + Definition: FixedScale: Common/MatDefs/ShaderNodes/Common/FixedScale.j3sn + InputMappings { + projectionMatrix = WorldParam.ProjectionMatrix + worldMatrix = WorldParam.WorldMatrix + cameraDir = WorldParam.CameraDirection + viewport = WorldParam.ViewPort + modelPosition = Attr.inPosition + cameraPos = WorldParam.CameraPosition + spriteHeight = MatParam.SpriteHeight + } + OutputMappings { + } + } + ShaderNode Billboard { + Definition: Billboard: Common/MatDefs/ShaderNodes/Common/Billboard.j3sn + InputMappings { + worldViewMatrix = WorldParam.WorldViewMatrix + projectionMatrix = WorldParam.ProjectionMatrix + modelPosition = Attr.inPosition + scale = FixedScale.scale + } + OutputMappings { + Global.position = projPosition + } + } + } + + FragmentShaderNodes { + ShaderNode TextureFetch { + Definition: TextureFetch: Common/MatDefs/ShaderNodes/Basic/TextureFetch.j3sn + InputMappings { + textureMap = MatParam.Texture + texCoord = TexCoord.vec2Variable + } + OutputMappings { + } + } + ShaderNode ColorMult { + Definition: ColorMult: Common/MatDefs/ShaderNodes/Basic/ColorMult.j3sn + InputMappings { + color1 = TextureFetch.outColor + color2 = TexCoord.vec4Variable + } + OutputMappings { + Global.color = outColor + } + } + } + + } +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/Billboard.j3sn b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/Billboard.j3sn new file mode 100644 index 000000000..a9421de4c --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/Billboard.j3sn @@ -0,0 +1,35 @@ +ShaderNodeDefinitions{ + ShaderNodeDefinition Billboard { + //Vertex/Fragment + Type: Vertex + + //Shader GLSL: + Shader GLSL100: Common/MatDefs/ShaderNodes/Common/Billboard100.frag + + Documentation{ + //type documentation here. This is optional but recommended + + //@input + @input mat4 worldViewMatrix The worldView matrix + @input mat4 projectionMatrix The projection matrix + @input vec3 modelPosition the vertex position + @input float scale the scale of the billboard (defautl 1) + + //@output + @output vec4 projPosition The position in projection space + } + Input { + //all the node inputs + // + mat4 worldViewMatrix + mat4 projectionMatrix + vec3 modelPosition + float scale 1 + } + Output { + //all the node outputs + // + vec4 projPosition + } + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/Billboard100.frag b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/Billboard100.frag new file mode 100644 index 000000000..d85880760 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/Billboard100.frag @@ -0,0 +1,19 @@ +void main(){ + // First colunm. + worldViewMatrix[0][0] = scale; + worldViewMatrix[0][1] = 0.0; + worldViewMatrix[0][2] = 0.0; + + // Second colunm. + worldViewMatrix[1][0] = 0.0; + worldViewMatrix[1][1] = scale; + worldViewMatrix[1][2] = 0.0; + + // Thrid colunm. + worldViewMatrix[2][0] = 0.0; + worldViewMatrix[2][1] = 0.0; + worldViewMatrix[2][2] = scale; + + vec4 position = worldViewMatrix * vec4(modelPosition,1.0); + projPosition = projectionMatrix * position; +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale.j3sn b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale.j3sn new file mode 100644 index 000000000..48a920474 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale.j3sn @@ -0,0 +1,41 @@ +ShaderNodeDefinitions{ + ShaderNodeDefinition FixedScale { + //Vertex/Fragment + Type: Vertex + + //Shader GLSL: + Shader GLSL100: Common/MatDefs/ShaderNodes/Common/FixedScale100.frag + + Documentation{ + //type documentation here. This is optional but recommended + + //@input + @input vec4 viewport The viewport information (right, top, left, bottom) + @input vec3 cameraDir The direction of the camera + @input vec3 cameraPos The position of the camera + @input mat4 worldMatrix The world matrix + @input mat4 projectionMatrix The projection matrix + @input vec3 modelPosition the vertex position + @input float spriteHeight the desired image height in pixel + + //@output + @output float scale The constant scale + } + Input { + //all the node inputs + // + vec4 viewport + vec3 cameraDir + vec3 cameraPos + mat4 worldMatrix + mat4 projectionMatrix + vec3 modelPosition + float spriteHeight 10.0 + } + Output { + //all the node outputs + // + float scale + } + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale100.frag b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale100.frag new file mode 100644 index 000000000..b15415b5f --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale100.frag @@ -0,0 +1,8 @@ +void main(){ + vec4 worldPos = worldMatrix * vec4(modelPosition, 1.0); + vec3 dir = worldPos.xyz - cameraPos; + float distance = dot(cameraDir, dir); + float m11 = projectionMatrix[1][1]; + float halfHeight = (viewport.w - viewport.y) * 0.5; + scale = ((distance/halfHeight) * spriteHeight)/m11; +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/texCoord.j3sn b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/texCoord.j3sn new file mode 100644 index 000000000..fcf1bddb9 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/texCoord.j3sn @@ -0,0 +1,57 @@ +ShaderNodeDefinitions{ + ShaderNodeDefinition TexCoord { + //Vertex/Fragment + Type: Vertex + + //Shader GLSL: + Shader GLSL100: Common/MatDefs/ShaderNodes/Common/texCoord100.frag + + Documentation{ + //type documentation here. This is optional but recommended + + //@input + @input vec2 texCoord The input texture Coord + @input vec2 texCoord2 The input texture Coord + @input vec2 texCoord3 The input texture Coord + @input vec2 texCoord4 The input texture Coord + @input vec2 texCoord5 The input texture Coord + @input vec2 texCoord6 The input texture Coord + @input vec2 texCoord7 The input texture Coord + @input vec2 texCoord8 The input texture Coord + + //@output + @output vec2 texCoord The input texture Coord + @output vec2 texCoord2 The input texture Coord + @output vec2 texCoord3 The input texture Coord + @output vec2 texCoord4 The input texture Coord + @output vec2 texCoord5 The input texture Coord + @output vec2 texCoord6 The input texture Coord + @output vec2 texCoord7 The input texture Coord + @output vec2 texCoord8 The input texture Coord + } + Input { + //all the node inputs + // + vec2 texCoord + vec2 texCoord2 + vec2 texCoord3 + vec2 texCoord4 + vec2 texCoord5 + vec2 texCoord6 + vec2 texCoord7 + vec2 texCoord8 + } + Output { + //all the node outputs + // + vec2 texCoord + vec2 texCoord2 + vec2 texCoord3 + vec2 texCoord4 + vec2 texCoord5 + vec2 texCoord6 + vec2 texCoord7 + vec2 texCoord8 + } + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/texCoord100.frag b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/texCoord100.frag new file mode 100644 index 000000000..61a3f2e67 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/texCoord100.frag @@ -0,0 +1,2 @@ +void main(){ +} diff --git a/jme3-core/src/main/resources/Common/Textures/dot.png b/jme3-core/src/main/resources/Common/Textures/dot.png new file mode 100644 index 0000000000000000000000000000000000000000..59605f195b4374625969c89bd5e6a0fddb95515c GIT binary patch literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4aTa()7Bet#3xhBt!>lt z;Q$f0^NS~Qt9S>uswDc zmpoJ2)~RZHGVr^wxqBxEgV##!2ckP3H?ZAvJ;IP`6% zDoXbmiWl5_va=+&`kC5!))kFAnfDwi{4+;|^RdM|NfyT3>#<>>q30Vee75nqZ`}I4 zlHnnb#M-T2j`{Gd-OBKob;ISTgTe~DWM4fD>#m^ literal 0 HcmV?d00001 diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java index 0beb2d0ae..a3d4210d0 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java @@ -40,12 +40,15 @@ public class TestArmature extends SimpleApplication { Joint root = new Joint("Root_Joint"); j1 = new Joint("Joint_1"); j2 = new Joint("Joint_2"); + Joint j3 = new Joint("Joint_3"); root.addChild(j1); j1.addChild(j2); + j2.addChild(j3); root.setLocalTranslation(new Vector3f(0, 0, 0.5f)); j1.setLocalTranslation(new Vector3f(0, 0.0f, -0.5f)); - j2.setLocalTranslation(new Vector3f(0, 0.0f, -0.2f)); - Joint[] joints = new Joint[]{root, j1, j2}; + j2.setLocalTranslation(new Vector3f(0, 0.0f, -0.3f)); + j3.setLocalTranslation(new Vector3f(0, 0, -0.2f)); + Joint[] joints = new Joint[]{root, j1, j2, j3}; final Armature armature = new Armature(joints); armature.setBindPose(); @@ -64,16 +67,16 @@ public class TestArmature extends SimpleApplication { }; Vector3f[] scales = new Vector3f[]{ new Vector3f(1, 1, 1), - new Vector3f(2, 2, 2), + new Vector3f(1, 1, 2), new Vector3f(1, 1, 1), }; Vector3f[] scales2 = new Vector3f[]{ new Vector3f(1, 1, 1), - new Vector3f(0.5f, 0.5f, 0.5f), + new Vector3f(1, 1, 0.5f), new Vector3f(1, 1, 1), }; - JointTrack track1 = new JointTrack(j1, times, null, rotations, null); + JointTrack track1 = new JointTrack(j1, times, null, rotations, scales); JointTrack track2 = new JointTrack(j2, times, null, rotations, null); clip.addTrack(track1); clip.addTrack(track2); @@ -98,7 +101,7 @@ public class TestArmature extends SimpleApplication { composer.setCurrentAnimClip("anim"); ArmatureDebugAppState debugAppState = new ArmatureDebugAppState(); - debugAppState.addArmature(ac, true); + debugAppState.addArmature(ac); stateManager.attach(debugAppState); rootNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal())); From e5057ad7fa1ac732c0ea12f79f88c15cc5c8a16b Mon Sep 17 00:00:00 2001 From: Nehon Date: Sat, 23 Dec 2017 18:36:12 +0100 Subject: [PATCH 09/54] Adds support for different joint model transform accumulation strategy --- .../src/main/java/com/jme3/anim/Armature.java | 35 ++++++++- .../src/main/java/com/jme3/anim/Joint.java | 43 +++++----- .../jme3/anim/MatrixJointModelTransform.java | 78 +++++++++++++++++++ .../anim/SeparateJointModelTransform.java | 74 ++++++++++++++++++ .../jme3/anim/util/JointModelTransform.java | 22 ++++++ .../jme3test/model/anim/TestArmature.java | 2 + 6 files changed, 229 insertions(+), 25 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/SeparateJointModelTransform.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java diff --git a/jme3-core/src/main/java/com/jme3/anim/Armature.java b/jme3-core/src/main/java/com/jme3/anim/Armature.java index 982e08d36..4e86b8fee 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Armature.java +++ b/jme3-core/src/main/java/com/jme3/anim/Armature.java @@ -1,5 +1,6 @@ package com.jme3.anim; +import com.jme3.anim.util.JointModelTransform; import com.jme3.export.*; import com.jme3.math.Matrix4f; import com.jme3.util.clone.Cloner; @@ -22,7 +23,7 @@ public class Armature implements JmeCloneable, Savable { * will cause it to go to the animated position. */ private transient Matrix4f[] skinningMatrixes; - + private Class modelTransformClass = MatrixJointModelTransform.class; /** * Serialization only @@ -44,9 +45,14 @@ public class Armature implements JmeCloneable, Savable { List rootJointList = new ArrayList<>(); for (int i = jointList.length - 1; i >= 0; i--) { - Joint b = jointList[i]; - if (b.getParent() == null) { - rootJointList.add(b); + Joint joint = jointList[i]; + try { + joint.setJointModelTransform(modelTransformClass.newInstance()); + } catch (InstantiationException | IllegalAccessException e) { + throw new IllegalArgumentException(e); + } + if (joint.getParent() == null) { + rootJointList.add(joint); } } rootJoints = rootJointList.toArray(new Joint[rootJointList.size()]); @@ -75,6 +81,27 @@ public class Armature implements JmeCloneable, Savable { } } + /** + * Sets the JointModelTransform implementation + * Default is {@link MatrixJointModelTransform} + * + * @param modelTransformClass + * @see {@link JointModelTransform},{@link MatrixJointModelTransform},{@link SeparateJointModelTransform}, + */ + public void setModelTransformClass(Class modelTransformClass) { + this.modelTransformClass = modelTransformClass; + if (jointList == null) { + return; + } + for (Joint joint : jointList) { + try { + joint.setJointModelTransform(modelTransformClass.newInstance()); + } catch (InstantiationException | IllegalAccessException e) { + throw new IllegalArgumentException(e); + } + } + } + /** * returns the array of all root joints of this Armatire * diff --git a/jme3-core/src/main/java/com/jme3/anim/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java index c2c92693b..c060ae86d 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Joint.java +++ b/jme3-core/src/main/java/com/jme3/anim/Joint.java @@ -1,5 +1,6 @@ package com.jme3.anim; +import com.jme3.anim.util.JointModelTransform; import com.jme3.export.*; import com.jme3.material.MatParamOverride; import com.jme3.math.*; @@ -41,9 +42,9 @@ public class Joint implements Savable, JmeCloneable { /** * The transform of the joint in model space. Relative to the origin of the model. + * this is either a MatrixJointModelTransform or a SeparateJointModelTransform */ - private Transform modelTransform = new Transform(); - private Matrix4f modelTransformMatrix = new Matrix4f(); + private JointModelTransform jointModelTransform; /** * The matrix used to transform affected vertices position into the joint model space. @@ -78,12 +79,7 @@ public class Joint implements Savable, JmeCloneable { * model transform with this bones' local transform. */ public final void updateModelTransforms() { - localTransform.toTransformMatrix(modelTransformMatrix); - if (parent != null) { - parent.modelTransformMatrix.mult(modelTransformMatrix, modelTransformMatrix); - } - modelTransform.fromTransformMatrix(modelTransformMatrix); - + jointModelTransform.updateModelTransform(localTransform, parent); updateAttachNode(); } @@ -102,11 +98,11 @@ public class Joint implements Savable, JmeCloneable { * The animated meshes are in the same coordinate system as the * attachments node: no further transforms are needed. */ - attachedNode.setLocalTransform(modelTransform); + attachedNode.setLocalTransform(getModelTransform()); } else { Spatial loopSpatial = targetGeometry; - Transform combined = modelTransform.clone(); + Transform combined = getModelTransform().clone(); /* * Climb the scene graph applying local transforms until the * attachments node's parent is reached. @@ -131,23 +127,18 @@ public class Joint implements Savable, JmeCloneable { * @param outTransform */ void getOffsetTransform(Matrix4f outTransform) { - outTransform.set(modelTransformMatrix).mult(inverseModelBindMatrix, outTransform); + jointModelTransform.getOffsetTransform(outTransform, inverseModelBindMatrix); } protected void setBindPose() { //Note that the whole Armature must be updated before calling this method. - inverseModelBindMatrix.set(modelTransformMatrix); + getModelTransform().toTransformMatrix(inverseModelBindMatrix); inverseModelBindMatrix.invertLocal(); baseLocalTransform.set(localTransform); } protected void resetToBindPose() { - //just using modelTransformMatrix as a temp matrix here - modelTransformMatrix.set(inverseModelBindMatrix).invertLocal(); // model transform = model bind - if (parent != null) { - parent.modelTransformMatrix.invert().mult(modelTransformMatrix, modelTransformMatrix); - } - localTransform.fromTransformMatrix(modelTransformMatrix); + jointModelTransform.applyBindPose(localTransform, inverseModelBindMatrix, parent); updateModelTransforms(); for (Joint child : children) { @@ -155,6 +146,14 @@ public class Joint implements Savable, JmeCloneable { } } + protected JointModelTransform getJointModelTransform() { + return jointModelTransform; + } + + protected void setJointModelTransform(JointModelTransform jointModelTransform) { + this.jointModelTransform = jointModelTransform; + } + public Vector3f getLocalTranslation() { return localTransform.getTranslation(); } @@ -246,7 +245,7 @@ public class Joint implements Savable, JmeCloneable { } public Transform getModelTransform() { - return modelTransform; + return jointModelTransform.getModelTransform(); } public Matrix4f getInverseModelBindMatrix() { @@ -270,8 +269,8 @@ public class Joint implements Savable, JmeCloneable { this.targetGeometry = cloner.clone(targetGeometry); this.baseLocalTransform = cloner.clone(baseLocalTransform); - this.localTransform = cloner.clone(baseLocalTransform); - this.modelTransform = cloner.clone(baseLocalTransform); + this.localTransform = cloner.clone(localTransform); + this.jointModelTransform = cloner.clone(jointModelTransform); this.inverseModelBindMatrix = cloner.clone(inverseModelBindMatrix); } @@ -287,6 +286,7 @@ public class Joint implements Savable, JmeCloneable { baseLocalTransform = (Transform) input.readSavable("baseLocalTransforms", baseLocalTransform); localTransform.set(baseLocalTransform); inverseModelBindMatrix = (Matrix4f) input.readSavable("inverseModelBindMatrix", inverseModelBindMatrix); + jointModelTransform = (JointModelTransform) input.readSavable("jointModelTransform", null); ArrayList childList = input.readSavableArrayList("children", null); for (int i = childList.size() - 1; i >= 0; i--) { @@ -304,6 +304,7 @@ public class Joint implements Savable, JmeCloneable { output.write(baseLocalTransform, "baseLocalTransform", new Transform()); output.write(inverseModelBindMatrix, "inverseModelBindMatrix", new Matrix4f()); output.writeSavableArrayList(children, "children", null); + output.write(jointModelTransform, "jointModelTransform", null); } } diff --git a/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java b/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java new file mode 100644 index 000000000..9c141b5ff --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java @@ -0,0 +1,78 @@ +package com.jme3.anim; + +import com.jme3.anim.util.JointModelTransform; +import com.jme3.export.*; +import com.jme3.math.Matrix4f; +import com.jme3.math.Transform; +import com.jme3.util.clone.Cloner; + +import java.io.IOException; + +/** + * This JointModelTransform implementation accumulate joints transforms in a Matrix4f to properly + * support non uniform scaling in an armature hierarchy + */ +public class MatrixJointModelTransform implements JointModelTransform { + + private Matrix4f modelTransformMatrix = new Matrix4f(); + private Transform modelTransform = new Transform(); + + @Override + public void updateModelTransform(Transform localTransform, Joint parent) { + localTransform.toTransformMatrix(modelTransformMatrix); + if (parent != null) { + ((MatrixJointModelTransform) parent.getJointModelTransform()).getModelTransformMatrix().mult(modelTransformMatrix, modelTransformMatrix); + } + modelTransform.fromTransformMatrix(modelTransformMatrix); + } + + public void getOffsetTransform(Matrix4f outTransform, Matrix4f inverseModelBindMatrix) { + outTransform.set(modelTransformMatrix).mult(inverseModelBindMatrix, outTransform); + } + + @Override + public void applyBindPose(Transform localTransform, Matrix4f inverseModelBindMatrix, Joint parent) { + modelTransformMatrix.set(inverseModelBindMatrix).invertLocal(); // model transform = model bind + if (parent != null) { + ((MatrixJointModelTransform) parent.getJointModelTransform()).getModelTransformMatrix().invert().mult(modelTransformMatrix, modelTransformMatrix); + } + localTransform.fromTransformMatrix(modelTransformMatrix); + } + + public Matrix4f getModelTransformMatrix() { + return modelTransformMatrix; + } + + @Override + public Transform getModelTransform() { + return modelTransform; + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule oc = ex.getCapsule(this); + oc.write(modelTransformMatrix, "modelTransformMatrix", new Matrix4f()); + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + modelTransformMatrix = (Matrix4f) ic.readSavable("modelTransformMatrix", new Matrix4f()); + modelTransform.fromTransformMatrix(modelTransformMatrix); + } + + @Override + public Object jmeClone() { + try { + MatrixJointModelTransform clone = (MatrixJointModelTransform) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + modelTransformMatrix = modelTransformMatrix.clone(); + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/SeparateJointModelTransform.java b/jme3-core/src/main/java/com/jme3/anim/SeparateJointModelTransform.java new file mode 100644 index 000000000..f5089f3d7 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/SeparateJointModelTransform.java @@ -0,0 +1,74 @@ +package com.jme3.anim; + +import com.jme3.anim.util.JointModelTransform; +import com.jme3.export.*; +import com.jme3.math.Matrix4f; +import com.jme3.math.Transform; +import com.jme3.util.clone.Cloner; + +import java.io.IOException; + +/** + * This JointModelTransform implementation accumulates model transform in a Transform class + * This does NOT support proper non uniform scale in the armature hierarchy. + * But the effect might be useful in some circumstances. + * Note that this is how the old animation system was working, so you might want to use this + * if your model has non uniform scale and was migrated from old j3o model. + */ +public class SeparateJointModelTransform implements JointModelTransform { + + private Transform modelTransform = new Transform(); + + @Override + public void updateModelTransform(Transform localTransform, Joint parent) { + modelTransform.set(localTransform); + if (parent != null) { + modelTransform.combineWithParent(parent.getModelTransform()); + } + } + + public void getOffsetTransform(Matrix4f outTransform, Matrix4f inverseModelBindMatrix) { + modelTransform.toTransformMatrix(outTransform).mult(inverseModelBindMatrix, outTransform); + } + + @Override + public void applyBindPose(Transform localTransform, Matrix4f inverseModelBindMatrix, Joint parent) { + localTransform.fromTransformMatrix(inverseModelBindMatrix); + localTransform.invert(); //model transform + if (parent != null) { + localTransform.combineWithParent(parent.getModelTransform().invert()); + } + } + + @Override + public Transform getModelTransform() { + return modelTransform; + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule oc = ex.getCapsule(this); + oc.write(modelTransform, "modelTransform", new Transform()); + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + modelTransform = (Transform) ic.readSavable("modelTransform", new Transform()); + } + + @Override + public Object jmeClone() { + try { + SeparateJointModelTransform clone = (SeparateJointModelTransform) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + modelTransform = modelTransform.clone(); + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java b/jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java new file mode 100644 index 000000000..48cc68c67 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java @@ -0,0 +1,22 @@ +package com.jme3.anim.util; + +import com.jme3.anim.Joint; +import com.jme3.export.Savable; +import com.jme3.math.Matrix4f; +import com.jme3.math.Transform; +import com.jme3.util.clone.JmeCloneable; + +/** + * Implementations of this interface holds accumulated model transform of a Joint. + * Implementation might choose different accumulation strategy. + */ +public interface JointModelTransform extends JmeCloneable, Savable { + + void updateModelTransform(Transform localTransform, Joint parent); + + void getOffsetTransform(Matrix4f outTransform, Matrix4f inverseModelBindMatrix); + + void applyBindPose(Transform localTransform, Matrix4f inverseModelBindMatrix, Joint parent); + + Transform getModelTransform(); +} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java index a3d4210d0..ba0d96510 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java @@ -51,6 +51,8 @@ public class TestArmature extends SimpleApplication { Joint[] joints = new Joint[]{root, j1, j2, j3}; final Armature armature = new Armature(joints); +// armature.setModelTransformClass(SeparateJointModelTransform.class); + armature.setBindPose(); AnimClip clip = new AnimClip("anim"); From 728e28857bf8e22bbeb8eb6db898430949ab25e6 Mon Sep 17 00:00:00 2001 From: Nehon Date: Sun, 24 Dec 2017 00:49:34 +0100 Subject: [PATCH 10/54] Adds a MigrationUtil to migrate from old system to new system --- .../main/java/com/jme3/anim/AnimComposer.java | 11 +- .../src/main/java/com/jme3/anim/Armature.java | 2 +- .../src/main/java/com/jme3/anim/Joint.java | 11 -- .../jme3/anim/util/AnimMigrationUtils.java | 175 +++++++++++++++++- .../debug/custom/ArmatureDebugAppState.java | 35 +++- .../model/anim/TestAnimMigration.java | 147 +++++++++++++++ .../jme3test/model/anim/TestArmature.java | 38 +--- 7 files changed, 360 insertions(+), 59 deletions(-) create mode 100644 jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index 6a74a0c2c..e70a4c3ea 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -4,8 +4,7 @@ import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.control.AbstractControl; -import java.util.HashMap; -import java.util.Map; +import java.util.*; /** * Created by Nehon on 20/12/2017. @@ -65,6 +64,14 @@ public class AnimComposer extends AbstractControl { time = 0; } + public Collection getAnimClips() { + return Collections.unmodifiableCollection(animClipMap.values()); + } + + public Collection getAnimClipsNames() { + return Collections.unmodifiableCollection(animClipMap.keySet()); + } + @Override protected void controlUpdate(float tpf) { if (currentAnimClip != null) { diff --git a/jme3-core/src/main/java/com/jme3/anim/Armature.java b/jme3-core/src/main/java/com/jme3/anim/Armature.java index 4e86b8fee..0d37c35f0 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Armature.java +++ b/jme3-core/src/main/java/com/jme3/anim/Armature.java @@ -169,7 +169,7 @@ public class Armature implements JmeCloneable, Savable { } /** - * Saves the current Armature state as it's bind pose. + * Saves the current Armature state as its bind pose. */ public void setBindPose() { //make sure all bones are updated diff --git a/jme3-core/src/main/java/com/jme3/anim/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java index c060ae86d..f85b95c38 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Joint.java +++ b/jme3-core/src/main/java/com/jme3/anim/Joint.java @@ -34,11 +34,6 @@ public class Joint implements Savable, JmeCloneable { * Or relative to the model's origin for the root joint. */ private Transform localTransform = new Transform(); - /** - * The base transform of the joint in local space. - * Those transform are the joint's initial value. - */ - private Transform baseLocalTransform = new Transform(); /** * The transform of the joint in model space. Relative to the origin of the model. @@ -134,7 +129,6 @@ public class Joint implements Savable, JmeCloneable { //Note that the whole Armature must be updated before calling this method. getModelTransform().toTransformMatrix(inverseModelBindMatrix); inverseModelBindMatrix.invertLocal(); - baseLocalTransform.set(localTransform); } protected void resetToBindPose() { @@ -267,8 +261,6 @@ public class Joint implements Savable, JmeCloneable { this.children = cloner.clone(children); this.attachedNode = cloner.clone(attachedNode); this.targetGeometry = cloner.clone(targetGeometry); - - this.baseLocalTransform = cloner.clone(baseLocalTransform); this.localTransform = cloner.clone(localTransform); this.jointModelTransform = cloner.clone(jointModelTransform); this.inverseModelBindMatrix = cloner.clone(inverseModelBindMatrix); @@ -283,8 +275,6 @@ public class Joint implements Savable, JmeCloneable { name = input.readString("name", null); attachedNode = (Node) input.readSavable("attachedNode", null); targetGeometry = (Geometry) input.readSavable("targetGeometry", null); - baseLocalTransform = (Transform) input.readSavable("baseLocalTransforms", baseLocalTransform); - localTransform.set(baseLocalTransform); inverseModelBindMatrix = (Matrix4f) input.readSavable("inverseModelBindMatrix", inverseModelBindMatrix); jointModelTransform = (JointModelTransform) input.readSavable("jointModelTransform", null); @@ -301,7 +291,6 @@ public class Joint implements Savable, JmeCloneable { output.write(name, "name", null); output.write(attachedNode, "attachedNode", null); output.write(targetGeometry, "targetGeometry", null); - output.write(baseLocalTransform, "baseLocalTransform", new Transform()); output.write(inverseModelBindMatrix, "inverseModelBindMatrix", new Matrix4f()); output.writeSavableArrayList(children, "children", null); output.write(jointModelTransform, "jointModelTransform", null); diff --git a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java index f11a0c43c..ba049c3ae 100644 --- a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java +++ b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java @@ -1,24 +1,189 @@ package com.jme3.anim.util; -import com.jme3.animation.AnimControl; -import com.jme3.scene.SceneGraphVisitor; -import com.jme3.scene.Spatial; +import com.jme3.anim.*; +import com.jme3.animation.*; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.scene.*; + +import java.util.*; public class AnimMigrationUtils { + private static AnimControlVisitor animControlVisitor = new AnimControlVisitor(); + private static SkeletonControlVisitor skeletonControlVisitor = new SkeletonControlVisitor(); + + public static Spatial migrate(Spatial source) { - //source.depthFirstTraversal(); + Map skeletonArmatureMap = new HashMap<>(); + animControlVisitor.setMappings(skeletonArmatureMap); + source.depthFirstTraversal(animControlVisitor); + skeletonControlVisitor.setMappings(skeletonArmatureMap); + source.depthFirstTraversal(skeletonControlVisitor); return source; } - private class AnimControlVisitor implements SceneGraphVisitor { + private static class AnimControlVisitor implements SceneGraphVisitor { + + Map skeletonArmatureMap; @Override public void visit(Spatial spatial) { AnimControl control = spatial.getControl(AnimControl.class); if (control != null) { + AnimComposer composer = new AnimComposer(); + Skeleton skeleton = control.getSkeleton(); + if (skeleton == null) { + //only bone anim for now + return; + } + + Joint[] joints = new Joint[skeleton.getBoneCount()]; + for (int i = 0; i < skeleton.getBoneCount(); i++) { + Bone b = skeleton.getBone(i); + Joint j = joints[i]; + if (j == null) { + j = fromBone(b); + joints[i] = j; + } + for (Bone bone : b.getChildren()) { + int index = skeleton.getBoneIndex(bone); + Joint joint = joints[index]; + if (joint == null) { + joint = fromBone(bone); + } + j.addChild(joint); + joints[index] = joint; + } + } + + Armature armature = new Armature(joints); + armature.setBindPose(); + skeletonArmatureMap.put(skeleton, armature); + + for (String animName : control.getAnimationNames()) { + Animation anim = control.getAnim(animName); + AnimClip clip = new AnimClip(animName); + Joint[] staticJoints = new Joint[joints.length]; + System.arraycopy(joints, 0, staticJoints, 0, joints.length); + for (Track track : anim.getTracks()) { + if (track instanceof BoneTrack) { + BoneTrack boneTrack = (BoneTrack) track; + int index = boneTrack.getTargetBoneIndex(); + Bone bone = skeleton.getBone(index); + Joint joint = joints[index]; + JointTrack jointTrack = fromBoneTrack(boneTrack, bone, joint); + clip.addTrack(jointTrack); + //this joint is animated let's remove it from the static joints + staticJoints[index] = null; + } + } + + for (int i = 0; i < staticJoints.length; i++) { + Joint j = staticJoints[i]; + if (j != null) { + // joint has no track , we create one with the default pose + float[] times = new float[]{0}; + Vector3f[] translations = new Vector3f[]{j.getLocalTranslation()}; + Quaternion[] rotations = new Quaternion[]{j.getLocalRotation()}; + Vector3f[] scales = new Vector3f[]{j.getLocalScale()}; + JointTrack track = new JointTrack(j, times, translations, rotations, scales); + clip.addTrack(track); + } + } + + composer.addAnimClip(clip); + } + spatial.removeControl(control); + spatial.addControl(composer); + } + } + + public void setMappings(Map skeletonArmatureMap) { + this.skeletonArmatureMap = skeletonArmatureMap; + } + } + + private static class SkeletonControlVisitor implements SceneGraphVisitor { + + Map skeletonArmatureMap; + + @Override + public void visit(Spatial spatial) { + SkeletonControl control = spatial.getControl(SkeletonControl.class); + if (control != null) { + Armature armature = skeletonArmatureMap.get(control.getSkeleton()); + SkinningControl skinningControl = new SkinningControl(armature); + Map> attachedSpatials = new HashMap<>(); + for (int i = 0; i < control.getSkeleton().getBoneCount(); i++) { + Bone b = control.getSkeleton().getBone(i); + Node n = control.getAttachmentsNode(b.getName()); + n.removeFromParent(); + if (!n.getChildren().isEmpty()) { + attachedSpatials.put(b.getName(), n.getChildren()); + } + } + spatial.removeControl(control); + spatial.addControl(skinningControl); + for (String name : attachedSpatials.keySet()) { + List spatials = attachedSpatials.get(name); + for (Spatial child : spatials) { + skinningControl.getAttachmentsNode(name).attachChild(child); + } + } } } + + public void setMappings(Map skeletonArmatureMap) { + this.skeletonArmatureMap = skeletonArmatureMap; + } } + + public static JointTrack fromBoneTrack(BoneTrack boneTrack, Bone bone, Joint joint) { + float[] times = new float[boneTrack.getTimes().length]; + int length = times.length; + System.arraycopy(boneTrack.getTimes(), 0, times, 0, length); + //translation + Vector3f[] translations = new Vector3f[length]; + if (boneTrack.getTranslations() != null) { + for (int i = 0; i < boneTrack.getTranslations().length; i++) { + Vector3f oldTrans = boneTrack.getTranslations()[i]; + Vector3f newTrans = new Vector3f(); + newTrans.set(bone.getBindPosition()).addLocal(oldTrans); + translations[i] = newTrans; + } + } + //rotation + Quaternion[] rotations = new Quaternion[length]; + if (boneTrack.getRotations() != null) { + for (int i = 0; i < boneTrack.getRotations().length; i++) { + Quaternion oldRot = boneTrack.getRotations()[i]; + Quaternion newRot = new Quaternion(); + newRot.set(bone.getBindRotation()).multLocal(oldRot); + rotations[i] = newRot; + } + } + //scale + Vector3f[] scales = new Vector3f[length]; + if (boneTrack.getScales() != null) { + for (int i = 0; i < boneTrack.getScales().length; i++) { + Vector3f oldScale = boneTrack.getScales()[i]; + Vector3f newScale = new Vector3f(); + newScale.set(bone.getBindScale()).multLocal(oldScale); + scales[i] = newScale; + } + } + + return new JointTrack(joint, times, translations, rotations, scales); + } + + private static Joint fromBone(Bone b) { + Joint j = new Joint(b.getName()); + j.setLocalTranslation(b.getBindPosition()); + j.setLocalRotation(b.getBindRotation()); + j.setLocalScale(b.getBindScale()); + return j; + } + } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java index 439316a95..47b78c1c1 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java @@ -6,8 +6,7 @@ package com.jme3.scene.debug.custom; import com.jme3.anim.*; import com.jme3.app.Application; -import com.jme3.app.state.AbstractAppState; -import com.jme3.app.state.AppStateManager; +import com.jme3.app.state.BaseAppState; import com.jme3.collision.CollisionResults; import com.jme3.input.MouseInput; import com.jme3.input.controls.ActionListener; @@ -22,15 +21,17 @@ import java.util.*; /** * @author Nehon */ -public class ArmatureDebugAppState extends AbstractAppState { +public class ArmatureDebugAppState extends BaseAppState { private Node debugNode = new Node("debugNode"); private Map armatures = new HashMap<>(); private Map selectedBones = new HashMap<>(); private Application app; + ViewPort vp; + @Override - public void initialize(AppStateManager stateManager, Application app) { - ViewPort vp = app.getRenderManager().createMainView("debug", app.getCamera()); + protected void initialize(Application app) { + vp = app.getRenderManager().createMainView("debug", app.getCamera()); vp.attachScene(debugNode); vp.setClearDepth(true); this.app = app; @@ -39,12 +40,26 @@ public class ArmatureDebugAppState extends AbstractAppState { } app.getInputManager().addListener(actionListener, "shoot"); app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); - super.initialize(stateManager, app); - debugNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal())); debugNode.addLight(new DirectionalLight(new Vector3f(1f, 1f, 1f).normalizeLocal(), new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f))); + vp.setEnabled(false); + } + + @Override + protected void cleanup(Application app) { + + } + + @Override + protected void onEnable() { + vp.setEnabled(true); + } + + @Override + protected void onDisable() { + vp.setEnabled(false); } @Override @@ -53,13 +68,13 @@ public class ArmatureDebugAppState extends AbstractAppState { debugNode.updateGeometricState(); } - public ArmatureDebugger addArmature(SkinningControl skinningControl) { + public ArmatureDebugger addArmatureFrom(SkinningControl skinningControl) { Armature armature = skinningControl.getArmature(); Spatial forSpatial = skinningControl.getSpatial(); - return addArmature(armature, forSpatial); + return addArmatureFrom(armature, forSpatial); } - public ArmatureDebugger addArmature(Armature armature, Spatial forSpatial) { + public ArmatureDebugger addArmatureFrom(Armature armature, Spatial forSpatial) { ArmatureDebugger ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature); ad.setLocalTransform(forSpatial.getWorldTransform()); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java new file mode 100644 index 000000000..333f09cc2 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -0,0 +1,147 @@ +package jme3test.model.anim; + +import com.jme3.anim.AnimComposer; +import com.jme3.anim.SkinningControl; +import com.jme3.anim.util.AnimMigrationUtils; +import com.jme3.app.ChaseCameraAppState; +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.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.math.*; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.debug.custom.ArmatureDebugAppState; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * Created by Nehon on 18/12/2017. + */ +public class TestAnimMigration extends SimpleApplication { + + ArmatureDebugAppState debugAppState; + AnimComposer composer; + Queue anims = new LinkedList<>(); + boolean playAnim = true; + + public static void main(String... argv) { + TestAnimMigration app = new TestAnimMigration(); + app.start(); + } + + @Override + public void simpleInitApp() { + setTimer(new EraseTimer()); + //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); + rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); + + Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + //Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + //Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); + //Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml"); + + AnimMigrationUtils.migrate(model); + + rootNode.attachChild(model); + + + debugAppState = new ArmatureDebugAppState(); + stateManager.attach(debugAppState); + + setupModel(model); + + flyCam.setEnabled(false); + + Node target = new Node("CamTarget"); + //target.setLocalTransform(model.getLocalTransform()); + target.move(0, 1, 0); + ChaseCameraAppState chaseCam = new ChaseCameraAppState(); + chaseCam.setTarget(target); + getStateManager().attach(chaseCam); + chaseCam.setInvertHorizontalAxis(true); + chaseCam.setInvertVerticalAxis(true); + chaseCam.setZoomSpeed(0.5f); + chaseCam.setMinVerticalRotation(-FastMath.HALF_PI); + chaseCam.setRotationSpeed(3); + chaseCam.setDefaultDistance(3); + chaseCam.setMinDistance(0.01f); + chaseCam.setZoomSpeed(0.01f); + chaseCam.setDefaultVerticalRotation(0.3f); + + initInputs(); + } + + public void initInputs() { + inputManager.addMapping("toggleAnim", new KeyTrigger(KeyInput.KEY_RETURN)); + + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed) { + playAnim = !playAnim; + if (playAnim) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAnimClip(anim); + System.err.println(anim); + } else { + composer.reset(); + } + } + } + }, "toggleAnim"); + inputManager.addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_RIGHT)); + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed && composer != null) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAnimClip(anim); + System.err.println(anim); + } + } + }, "nextAnim"); + } + + private void setupModel(Spatial model) { + if (composer != null) { + return; + } + composer = model.getControl(AnimComposer.class); + if (composer != null) { + + SkinningControl sc = model.getControl(SkinningControl.class); + debugAppState.addArmatureFrom(sc); + + anims.clear(); + for (String name : composer.getAnimClipsNames()) { + anims.add(name); + } + if (anims.isEmpty()) { + return; + } + if (playAnim) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAnimClip(anim); + System.err.println(anim); + } + + } else { + if (model instanceof Node) { + Node n = (Node) model; + for (Spatial child : n.getChildren()) { + setupModel(child); + } + } + } + + } +} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java index ba0d96510..80ecb0132 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java @@ -6,7 +6,6 @@ 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.light.DirectionalLight; import com.jme3.material.Material; import com.jme3.math.*; import com.jme3.scene.*; @@ -37,6 +36,7 @@ public class TestArmature extends SimpleApplication { //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); viewPort.setBackgroundColor(ColorRGBA.DarkGray); + //create armature Joint root = new Joint("Root_Joint"); j1 = new Joint("Joint_1"); j2 = new Joint("Joint_2"); @@ -52,9 +52,9 @@ public class TestArmature extends SimpleApplication { final Armature armature = new Armature(joints); // armature.setModelTransformClass(SeparateJointModelTransform.class); - armature.setBindPose(); + //create animations AnimClip clip = new AnimClip("anim"); float[] times = new float[]{0, 2, 4}; Quaternion[] rotations = new Quaternion[]{ @@ -83,15 +83,18 @@ public class TestArmature extends SimpleApplication { clip.addTrack(track1); clip.addTrack(track2); + //create the animComposer control final AnimComposer composer = new AnimComposer(); composer.addAnimClip(clip); + //create the SkinningControl SkinningControl ac = new SkinningControl(armature); ac.setHardwareSkinningPreferred(false); Node node = new Node("Test Armature"); rootNode.attachChild(node); + //Create the mesh to deform. Geometry cylinder = new Geometry("cylinder", createMesh()); Material m = new Material(assetManager, "Common/MatDefs/Misc/fakeLighting.j3md"); m.setColor("Color", ColorRGBA.randomColor()); @@ -103,14 +106,9 @@ public class TestArmature extends SimpleApplication { composer.setCurrentAnimClip("anim"); ArmatureDebugAppState debugAppState = new ArmatureDebugAppState(); - debugAppState.addArmature(ac); + debugAppState.addArmatureFrom(ac); stateManager.attach(debugAppState); - rootNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal())); - - rootNode.addLight(new DirectionalLight(new Vector3f(1f, 1f, 1f).normalizeLocal(), new ColorRGBA(0.7f, 0.7f, 0.7f, 1.0f))); - - flyCam.setEnabled(false); ChaseCameraAppState chaseCam = new ChaseCameraAppState(); @@ -132,12 +130,10 @@ public class TestArmature extends SimpleApplication { @Override public void onAction(String name, boolean isPressed, float tpf) { if (isPressed) { - play = false; composer.reset(); armature.resetToBindPose(); } else { - play = true; composer.setCurrentAnimClip("anim"); } } @@ -202,12 +198,10 @@ public class TestArmature extends SimpleApplication { c.updateCounts(); c.updateBound(); - //the mesh has some skinning let's create needed buffers for HW skinning - //creating empty buffers for HW skinning - //the buffers will be setup if ever used. + VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight); VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex); - //setting usage to cpuOnly so that the buffer is not send empty to the GPU + indicesHW.setUsage(VertexBuffer.Usage.CpuOnly); weightsHW.setUsage(VertexBuffer.Usage.CpuOnly); c.setBuffer(weightsHW); @@ -220,20 +214,4 @@ public class TestArmature extends SimpleApplication { } - float time = 0; - boolean play = true; - - @Override - public void simpleUpdate(float tpf) { - - -// if (play == false) { -// return; -// } -// time += tpf; -// float rot = FastMath.sin(time); -// j1.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * rot, Vector3f.UNIT_Z)); -// j2.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * rot, Vector3f.UNIT_Z)); - - } } From bbb3cf59b3f2bef1a681b439ecb2ff5cb438a0a9 Mon Sep 17 00:00:00 2001 From: Nehon Date: Sun, 24 Dec 2017 15:16:30 +0100 Subject: [PATCH 11/54] New anim system proper serialization --- .../src/main/java/com/jme3/anim/AnimClip.java | 2 +- .../main/java/com/jme3/anim/AnimComposer.java | 37 +++ .../src/main/java/com/jme3/anim/Armature.java | 36 ++- .../src/main/java/com/jme3/anim/Joint.java | 4 +- .../jme3/anim/MatrixJointModelTransform.java | 32 --- .../anim/SeparateJointModelTransform.java | 33 +-- .../java/com/jme3/anim/TransformTrack.java | 41 ++-- .../jme3/anim/util/JointModelTransform.java | 4 +- .../model/anim/TestAnimMigration.java | 6 +- .../model/anim/TestAnimSerialization.java | 168 ++++++++++++++ .../jme3test/model/anim/TestArmature.java | 2 +- .../model/anim/TestBaseAnimSerialization.java | 217 ++++++++++++++++++ 12 files changed, 482 insertions(+), 100 deletions(-) create mode 100644 jme3-examples/src/main/java/jme3test/model/anim/TestAnimSerialization.java create mode 100644 jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java index 00eff2c05..8bd64e0e1 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java @@ -105,7 +105,7 @@ public class AnimClip implements Tween, JmeCloneable, Savable { if (arr != null) { tracks = new SafeArrayList<>(Tween.class); for (Savable savable : arr) { - tracks.add((Tween) savable); + addTrack((Tween) savable); } } } diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index e70a4c3ea..731f92a19 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -1,9 +1,12 @@ package com.jme3.anim; +import com.jme3.export.*; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.control.AbstractControl; +import com.jme3.util.clone.Cloner; +import java.io.IOException; import java.util.*; /** @@ -87,4 +90,38 @@ public class AnimComposer extends AbstractControl { protected void controlRender(RenderManager rm, ViewPort vp) { } + + @Override + public Object jmeClone() { + try { + AnimComposer clone = (AnimComposer) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + super.cloneFields(cloner, original); + Map clips = new HashMap<>(); + for (String key : animClipMap.keySet()) { + clips.put(key, cloner.clone(animClipMap.get(key))); + } + animClipMap = clips; + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule ic = im.getCapsule(this); + animClipMap = (Map) ic.readStringSavableMap("animClipMap", new HashMap()); + } + + @Override + public void write(JmeExporter ex) throws IOException { + super.write(ex); + OutputCapsule oc = ex.getCapsule(this); + oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap()); + } } diff --git a/jme3-core/src/main/java/com/jme3/anim/Armature.java b/jme3-core/src/main/java/com/jme3/anim/Armature.java index 0d37c35f0..370618e7f 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Armature.java +++ b/jme3-core/src/main/java/com/jme3/anim/Armature.java @@ -1,6 +1,7 @@ package com.jme3.anim; import com.jme3.anim.util.JointModelTransform; +import com.jme3.asset.AssetLoadException; import com.jme3.export.*; import com.jme3.math.Matrix4f; import com.jme3.util.clone.Cloner; @@ -46,11 +47,7 @@ public class Armature implements JmeCloneable, Savable { List rootJointList = new ArrayList<>(); for (int i = jointList.length - 1; i >= 0; i--) { Joint joint = jointList[i]; - try { - joint.setJointModelTransform(modelTransformClass.newInstance()); - } catch (InstantiationException | IllegalAccessException e) { - throw new IllegalArgumentException(e); - } + instanciateJointModelTransform(joint); if (joint.getParent() == null) { rootJointList.add(joint); } @@ -94,11 +91,15 @@ public class Armature implements JmeCloneable, Savable { return; } for (Joint joint : jointList) { - try { - joint.setJointModelTransform(modelTransformClass.newInstance()); - } catch (InstantiationException | IllegalAccessException e) { - throw new IllegalArgumentException(e); - } + instanciateJointModelTransform(joint); + } + } + + private void instanciateJointModelTransform(Joint joint) { + try { + joint.setJointModelTransform(modelTransformClass.newInstance()); + } catch (InstantiationException | IllegalAccessException e) { + throw new IllegalArgumentException(e); } } @@ -226,6 +227,9 @@ public class Armature implements JmeCloneable, Savable { this.rootJoints = cloner.clone(rootJoints); this.jointList = cloner.clone(jointList); this.skinningMatrixes = cloner.clone(skinningMatrixes); + for (Joint joint : jointList) { + instanciateJointModelTransform(joint); + } } @@ -241,11 +245,22 @@ public class Armature implements JmeCloneable, Savable { jointList = new Joint[jointListAsSavable.length]; System.arraycopy(jointListAsSavable, 0, jointList, 0, jointListAsSavable.length); + String className = input.readString("modelTransformClass", MatrixJointModelTransform.class.getCanonicalName()); + try { + modelTransformClass = (Class) Class.forName(className); + } catch (ClassNotFoundException e) { + throw new AssetLoadException("Cannnot find class for name " + className); + } + + for (Joint joint : jointList) { + instanciateJointModelTransform(joint); + } createSkinningMatrices(); for (Joint rootJoint : rootJoints) { rootJoint.update(); } + resetToBindPose(); } @Override @@ -253,5 +268,6 @@ public class Armature implements JmeCloneable, Savable { OutputCapsule output = ex.getCapsule(this); output.write(rootJoints, "rootJoints", null); output.write(jointList, "jointList", null); + output.write(modelTransformClass.getCanonicalName(), "modelTransformClass", MatrixJointModelTransform.class.getCanonicalName()); } } diff --git a/jme3-core/src/main/java/com/jme3/anim/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java index f85b95c38..6ff53f59c 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Joint.java +++ b/jme3-core/src/main/java/com/jme3/anim/Joint.java @@ -259,10 +259,10 @@ public class Joint implements Savable, JmeCloneable { @Override public void cloneFields(Cloner cloner, Object original) { this.children = cloner.clone(children); + this.parent = cloner.clone(parent); this.attachedNode = cloner.clone(attachedNode); this.targetGeometry = cloner.clone(targetGeometry); this.localTransform = cloner.clone(localTransform); - this.jointModelTransform = cloner.clone(jointModelTransform); this.inverseModelBindMatrix = cloner.clone(inverseModelBindMatrix); } @@ -276,7 +276,6 @@ public class Joint implements Savable, JmeCloneable { attachedNode = (Node) input.readSavable("attachedNode", null); targetGeometry = (Geometry) input.readSavable("targetGeometry", null); inverseModelBindMatrix = (Matrix4f) input.readSavable("inverseModelBindMatrix", inverseModelBindMatrix); - jointModelTransform = (JointModelTransform) input.readSavable("jointModelTransform", null); ArrayList childList = input.readSavableArrayList("children", null); for (int i = childList.size() - 1; i >= 0; i--) { @@ -293,7 +292,6 @@ public class Joint implements Savable, JmeCloneable { output.write(targetGeometry, "targetGeometry", null); output.write(inverseModelBindMatrix, "inverseModelBindMatrix", new Matrix4f()); output.writeSavableArrayList(children, "children", null); - output.write(jointModelTransform, "jointModelTransform", null); } } diff --git a/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java b/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java index 9c141b5ff..27964f325 100644 --- a/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java +++ b/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java @@ -1,12 +1,8 @@ package com.jme3.anim; import com.jme3.anim.util.JointModelTransform; -import com.jme3.export.*; import com.jme3.math.Matrix4f; import com.jme3.math.Transform; -import com.jme3.util.clone.Cloner; - -import java.io.IOException; /** * This JointModelTransform implementation accumulate joints transforms in a Matrix4f to properly @@ -47,32 +43,4 @@ public class MatrixJointModelTransform implements JointModelTransform { public Transform getModelTransform() { return modelTransform; } - - @Override - public void write(JmeExporter ex) throws IOException { - OutputCapsule oc = ex.getCapsule(this); - oc.write(modelTransformMatrix, "modelTransformMatrix", new Matrix4f()); - } - - @Override - public void read(JmeImporter im) throws IOException { - InputCapsule ic = im.getCapsule(this); - modelTransformMatrix = (Matrix4f) ic.readSavable("modelTransformMatrix", new Matrix4f()); - modelTransform.fromTransformMatrix(modelTransformMatrix); - } - - @Override - public Object jmeClone() { - try { - MatrixJointModelTransform clone = (MatrixJointModelTransform) super.clone(); - return clone; - } catch (CloneNotSupportedException ex) { - throw new AssertionError(); - } - } - - @Override - public void cloneFields(Cloner cloner, Object original) { - modelTransformMatrix = modelTransformMatrix.clone(); - } } diff --git a/jme3-core/src/main/java/com/jme3/anim/SeparateJointModelTransform.java b/jme3-core/src/main/java/com/jme3/anim/SeparateJointModelTransform.java index f5089f3d7..c06b97ba4 100644 --- a/jme3-core/src/main/java/com/jme3/anim/SeparateJointModelTransform.java +++ b/jme3-core/src/main/java/com/jme3/anim/SeparateJointModelTransform.java @@ -1,12 +1,8 @@ package com.jme3.anim; import com.jme3.anim.util.JointModelTransform; -import com.jme3.export.*; import com.jme3.math.Matrix4f; import com.jme3.math.Transform; -import com.jme3.util.clone.Cloner; - -import java.io.IOException; /** * This JointModelTransform implementation accumulates model transform in a Transform class @@ -33,8 +29,7 @@ public class SeparateJointModelTransform implements JointModelTransform { @Override public void applyBindPose(Transform localTransform, Matrix4f inverseModelBindMatrix, Joint parent) { - localTransform.fromTransformMatrix(inverseModelBindMatrix); - localTransform.invert(); //model transform + localTransform.fromTransformMatrix(inverseModelBindMatrix.invert()); if (parent != null) { localTransform.combineWithParent(parent.getModelTransform().invert()); } @@ -45,30 +40,4 @@ public class SeparateJointModelTransform implements JointModelTransform { return modelTransform; } - @Override - public void write(JmeExporter ex) throws IOException { - OutputCapsule oc = ex.getCapsule(this); - oc.write(modelTransform, "modelTransform", new Transform()); - } - - @Override - public void read(JmeImporter im) throws IOException { - InputCapsule ic = im.getCapsule(this); - modelTransform = (Transform) ic.readSavable("modelTransform", new Transform()); - } - - @Override - public Object jmeClone() { - try { - SeparateJointModelTransform clone = (SeparateJointModelTransform) super.clone(); - return clone; - } catch (CloneNotSupportedException ex) { - throw new AssertionError(); - } - } - - @Override - public void cloneFields(Cloner cloner, Object original) { - modelTransform = modelTransform.clone(); - } } diff --git a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java index 5a2c329d9..5e0064ac5 100644 --- a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java +++ b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java @@ -307,6 +307,7 @@ public abstract class TransformTrack implements Tween, JmeCloneable, Savable { rotations = (CompactQuaternionArray) ic.readSavable("rotations", null); times = ic.readFloatArray("times", null); scales = (CompactVector3Array) ic.readSavable("scales", null); + setTimes(times); } @Override @@ -322,23 +323,33 @@ public abstract class TransformTrack implements Tween, JmeCloneable, Savable { public void cloneFields(Cloner cloner, Object original) { int tablesLength = times.length; - times = this.times.clone(); - Vector3f[] sourceTranslations = this.getTranslations(); - Quaternion[] sourceRotations = this.getRotations(); - Vector3f[] sourceScales = this.getScales(); - - Vector3f[] translations = new Vector3f[tablesLength]; - Quaternion[] rotations = new Quaternion[tablesLength]; - Vector3f[] scales = new Vector3f[tablesLength]; - for (int i = 0; i < tablesLength; ++i) { - translations[i] = sourceTranslations[i].clone(); - rotations[i] = sourceRotations[i].clone(); - scales[i] = sourceScales != null ? sourceScales[i].clone() : new Vector3f(1.0f, 1.0f, 1.0f); + setTimes(this.times.clone()); + if (translations != null) { + Vector3f[] sourceTranslations = this.getTranslations(); + Vector3f[] translations = new Vector3f[tablesLength]; + for (int i = 0; i < tablesLength; ++i) { + translations[i] = sourceTranslations[i].clone(); + } + setKeyframesTranslation(translations); + } + if (rotations != null) { + Quaternion[] sourceRotations = this.getRotations(); + Quaternion[] rotations = new Quaternion[tablesLength]; + for (int i = 0; i < tablesLength; ++i) { + rotations[i] = sourceRotations[i].clone(); + } + setKeyframesRotation(rotations); + } + + if (scales != null) { + Vector3f[] sourceScales = this.getScales(); + Vector3f[] scales = new Vector3f[tablesLength]; + for (int i = 0; i < tablesLength; ++i) { + scales[i] = sourceScales[i].clone(); + } + setKeyframesScale(scales); } - setKeyframesTranslation(translations); - setKeyframesScale(scales); - setKeyframesRotation(rotations); setFrameInterpolator(this.interpolator); } } diff --git a/jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java b/jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java index 48cc68c67..c73c03316 100644 --- a/jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java +++ b/jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java @@ -1,16 +1,14 @@ package com.jme3.anim.util; import com.jme3.anim.Joint; -import com.jme3.export.Savable; import com.jme3.math.Matrix4f; import com.jme3.math.Transform; -import com.jme3.util.clone.JmeCloneable; /** * Implementations of this interface holds accumulated model transform of a Joint. * Implementation might choose different accumulation strategy. */ -public interface JointModelTransform extends JmeCloneable, Savable { +public interface JointModelTransform { void updateModelTransform(Transform localTransform, Joint parent); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index 333f09cc2..1525165ad 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -41,9 +41,9 @@ public class TestAnimMigration extends SimpleApplication { rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); - Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + //Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); //Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); - //Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); + Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); //Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml"); AnimMigrationUtils.migrate(model); @@ -52,7 +52,7 @@ public class TestAnimMigration extends SimpleApplication { debugAppState = new ArmatureDebugAppState(); - stateManager.attach(debugAppState); + //stateManager.attach(debugAppState); setupModel(model); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimSerialization.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimSerialization.java new file mode 100644 index 000000000..6b2196813 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimSerialization.java @@ -0,0 +1,168 @@ +package jme3test.model.anim; + +import com.jme3.anim.AnimComposer; +import com.jme3.anim.SkinningControl; +import com.jme3.anim.util.AnimMigrationUtils; +import com.jme3.app.ChaseCameraAppState; +import com.jme3.app.SimpleApplication; +import com.jme3.asset.plugins.FileLocator; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.math.*; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.debug.custom.ArmatureDebugAppState; +import com.jme3.system.JmeSystem; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.Queue; + +/** + * Created by Nehon on 18/12/2017. + */ +public class TestAnimSerialization extends SimpleApplication { + + ArmatureDebugAppState debugAppState; + AnimComposer composer; + Queue anims = new LinkedList<>(); + boolean playAnim = true; + File file; + + public static void main(String... argv) { + TestAnimSerialization app = new TestAnimSerialization(); + app.start(); + } + + @Override + public void simpleInitApp() { + setTimer(new EraseTimer()); + //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); + rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); + + Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + + AnimMigrationUtils.migrate(model); + + File storageFolder = JmeSystem.getStorageFolder(); + file = new File(storageFolder.getPath() + File.separator + "newJaime.j3o"); + BinaryExporter be = new BinaryExporter(); + try { + be.save(model, file); + } catch (IOException e) { + e.printStackTrace(); + } + + assetManager.registerLocator(storageFolder.getPath(), FileLocator.class); + model = assetManager.loadModel("newJaime.j3o"); + + rootNode.attachChild(model); + + debugAppState = new ArmatureDebugAppState(); + stateManager.attach(debugAppState); + + setupModel(model); + + flyCam.setEnabled(false); + + Node target = new Node("CamTarget"); + //target.setLocalTransform(model.getLocalTransform()); + target.move(0, 1, 0); + ChaseCameraAppState chaseCam = new ChaseCameraAppState(); + chaseCam.setTarget(target); + getStateManager().attach(chaseCam); + chaseCam.setInvertHorizontalAxis(true); + chaseCam.setInvertVerticalAxis(true); + chaseCam.setZoomSpeed(0.5f); + chaseCam.setMinVerticalRotation(-FastMath.HALF_PI); + chaseCam.setRotationSpeed(3); + chaseCam.setDefaultDistance(3); + chaseCam.setMinDistance(0.01f); + chaseCam.setZoomSpeed(0.01f); + chaseCam.setDefaultVerticalRotation(0.3f); + + initInputs(); + } + + public void initInputs() { + inputManager.addMapping("toggleAnim", new KeyTrigger(KeyInput.KEY_RETURN)); + + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed) { + playAnim = !playAnim; + if (playAnim) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAnimClip(anim); + System.err.println(anim); + } else { + composer.reset(); + } + } + } + }, "toggleAnim"); + inputManager.addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_RIGHT)); + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed && composer != null) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAnimClip(anim); + System.err.println(anim); + } + } + }, "nextAnim"); + } + + private void setupModel(Spatial model) { + if (composer != null) { + return; + } + composer = model.getControl(AnimComposer.class); + if (composer != null) { + + SkinningControl sc = model.getControl(SkinningControl.class); + + debugAppState.addArmatureFrom(sc); + anims.clear(); + for (String name : composer.getAnimClipsNames()) { + anims.add(name); + } + if (anims.isEmpty()) { + return; + } + if (playAnim) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAnimClip(anim); + System.err.println(anim); + } + + } else { + if (model instanceof Node) { + Node n = (Node) model; + for (Spatial child : n.getChildren()) { + setupModel(child); + } + } + } + + } + + + @Override + public void destroy() { + super.destroy(); + file.delete(); + } +} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java index 80ecb0132..eefb08c75 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java @@ -51,7 +51,7 @@ public class TestArmature extends SimpleApplication { Joint[] joints = new Joint[]{root, j1, j2, j3}; final Armature armature = new Armature(joints); -// armature.setModelTransformClass(SeparateJointModelTransform.class); + //armature.setModelTransformClass(SeparateJointModelTransform.class); armature.setBindPose(); //create animations diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java b/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java new file mode 100644 index 000000000..512a777f3 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java @@ -0,0 +1,217 @@ +package jme3test.model.anim; + +import com.jme3.anim.*; +import com.jme3.app.ChaseCameraAppState; +import com.jme3.app.SimpleApplication; +import com.jme3.asset.plugins.FileLocator; +import com.jme3.export.binary.BinaryExporter; +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.*; +import com.jme3.scene.*; +import com.jme3.scene.debug.custom.ArmatureDebugAppState; +import com.jme3.scene.shape.Cylinder; +import com.jme3.system.JmeSystem; + +import java.io.File; +import java.io.IOException; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; + +/** + * Created by Nehon on 18/12/2017. + */ +public class TestBaseAnimSerialization extends SimpleApplication { + + Joint j1; + Joint j2; + AnimComposer composer; + Armature armature; + File file; + + public static void main(String... argv) { + TestBaseAnimSerialization app = new TestBaseAnimSerialization(); + app.start(); + } + + @Override + public void simpleInitApp() { + setTimer(new EraseTimer()); + renderManager.setSinglePassLightBatchSize(2); + //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + + //create armature + Joint root = new Joint("Root_Joint"); + j1 = new Joint("Joint_1"); + j2 = new Joint("Joint_2"); + Joint j3 = new Joint("Joint_3"); + root.addChild(j1); + j1.addChild(j2); + j2.addChild(j3); + root.setLocalTranslation(new Vector3f(0, 0, 0.5f)); + j1.setLocalTranslation(new Vector3f(0, 0.0f, -0.5f)); + j2.setLocalTranslation(new Vector3f(0, 0.0f, -0.3f)); + j3.setLocalTranslation(new Vector3f(0, 0, -0.2f)); + Joint[] joints = new Joint[]{root, j1, j2, j3}; + + armature = new Armature(joints); + //armature.setModelTransformClass(SeparateJointModelTransform.class); + armature.setBindPose(); + + //create animations + AnimClip clip = new AnimClip("anim"); + float[] times = new float[]{0, 2, 4}; + Quaternion[] rotations = new Quaternion[]{ + new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X), + new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X), + new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X) + }; + Vector3f[] translations = new Vector3f[]{ + new Vector3f(0, 0.2f, 0), + new Vector3f(0, 1.0f, 0), + new Vector3f(0, 0.2f, 0), + }; + Vector3f[] scales = new Vector3f[]{ + new Vector3f(1, 1, 1), + new Vector3f(1, 1, 2), + new Vector3f(1, 1, 1), + }; + Vector3f[] scales2 = new Vector3f[]{ + new Vector3f(1, 1, 1), + new Vector3f(1, 1, 0.5f), + new Vector3f(1, 1, 1), + }; + + JointTrack track1 = new JointTrack(j1, times, null, rotations, scales); + JointTrack track2 = new JointTrack(j2, times, null, rotations, null); + clip.addTrack(track1); + clip.addTrack(track2); + + //create the animComposer control + composer = new AnimComposer(); + composer.addAnimClip(clip); + + //create the SkinningControl + SkinningControl ac = new SkinningControl(armature); + Node node = new Node("Test Armature"); + + //Create the mesh to deform. + Geometry cylinder = new Geometry("cylinder", createMesh()); + Material m = new Material(assetManager, "Common/MatDefs/Misc/fakeLighting.j3md"); + m.setColor("Color", ColorRGBA.randomColor()); + cylinder.setMaterial(m); + node.attachChild(cylinder); + node.addControl(composer); + node.addControl(ac); + + File storageFolder = JmeSystem.getStorageFolder(); + file = new File(storageFolder.getPath() + File.separator + "test.j3o"); + BinaryExporter be = new BinaryExporter(); + try { + be.save(node, file); + } catch (IOException e) { + e.printStackTrace(); + } + + assetManager.registerLocator(storageFolder.getPath(), FileLocator.class); + Node newNode = (Node) assetManager.loadModel("test.j3o"); + + rootNode.attachChild(newNode); + + composer = newNode.getControl(AnimComposer.class); + ac = newNode.getControl(SkinningControl.class); + ac.setHardwareSkinningPreferred(false); + armature = ac.getArmature(); + composer.setCurrentAnimClip("anim"); + + ArmatureDebugAppState debugAppState = new ArmatureDebugAppState(); + debugAppState.addArmatureFrom(ac); + stateManager.attach(debugAppState); + + flyCam.setEnabled(false); + + ChaseCameraAppState chaseCam = new ChaseCameraAppState(); + chaseCam.setTarget(node); + getStateManager().attach(chaseCam); + chaseCam.setInvertHorizontalAxis(true); + chaseCam.setInvertVerticalAxis(true); + chaseCam.setZoomSpeed(0.5f); + chaseCam.setMinVerticalRotation(-FastMath.HALF_PI); + chaseCam.setRotationSpeed(3); + chaseCam.setDefaultDistance(3); + chaseCam.setMinDistance(0.01f); + chaseCam.setZoomSpeed(0.01f); + chaseCam.setDefaultVerticalRotation(0.3f); + + + inputManager.addMapping("bind", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed) { + composer.reset(); + armature.resetToBindPose(); + + } else { + composer.setCurrentAnimClip("anim"); + } + } + }, "bind"); + } + + private Mesh createMesh() { + Cylinder c = new Cylinder(30, 16, 0.1f, 1, true); + + ShortBuffer jointIndex = (ShortBuffer) VertexBuffer.createBuffer(VertexBuffer.Format.UnsignedShort, 4, c.getVertexCount()); + jointIndex.rewind(); + c.setMaxNumWeights(1); + FloatBuffer jointWeight = (FloatBuffer) VertexBuffer.createBuffer(VertexBuffer.Format.Float, 4, c.getVertexCount()); + jointWeight.rewind(); + VertexBuffer vb = c.getBuffer(VertexBuffer.Type.Position); + FloatBuffer fvb = (FloatBuffer) vb.getData(); + fvb.rewind(); + for (int i = 0; i < c.getVertexCount(); i++) { + fvb.get(); + fvb.get(); + float z = fvb.get(); + int index = 0; + if (z > 0) { + index = 0; + } else if (z > -0.2) { + index = 1; + } else { + index = 2; + } + jointIndex.put((short) index).put((short) 0).put((short) 0).put((short) 0); + jointWeight.put(1f).put(0f).put(0f).put(0f); + + } + c.setBuffer(VertexBuffer.Type.BoneIndex, 4, jointIndex); + c.setBuffer(VertexBuffer.Type.BoneWeight, 4, jointWeight); + + c.updateCounts(); + c.updateBound(); + + VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight); + VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex); + + indicesHW.setUsage(VertexBuffer.Usage.CpuOnly); + weightsHW.setUsage(VertexBuffer.Usage.CpuOnly); + c.setBuffer(weightsHW); + c.setBuffer(indicesHW); + c.generateBindPose(); + + c.prepareForAnim(false); + + return c; + } + + @Override + public void destroy() { + super.destroy(); + file.delete(); + } +} From abe094e74a86411dbcd4b22d5a8ebc3588b2211b Mon Sep 17 00:00:00 2001 From: Nehon Date: Mon, 25 Dec 2017 00:45:46 +0100 Subject: [PATCH 12/54] Gltf loader now supports the new animation system --- .../main/java/com/jme3/anim/JointTrack.java | 2 +- .../main/java/com/jme3/anim/SpatialTrack.java | 114 +++++++ .../java/jme3test/model/TestGltfLoading.java | 102 +++--- .../java/jme3test/model/TestGltfLoading2.java | 320 ----------------- .../jme3/scene/plugins/gltf/GltfLoader.java | 322 +++++------------- .../jme3/scene/plugins/gltf/GltfUtils.java | 11 +- 6 files changed, 262 insertions(+), 609 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java delete mode 100644 jme3-examples/src/main/java/jme3test/model/TestGltfLoading2.java diff --git a/jme3-core/src/main/java/com/jme3/anim/JointTrack.java b/jme3-core/src/main/java/com/jme3/anim/JointTrack.java index 1d2b4fd51..bd5f24025 100644 --- a/jme3-core/src/main/java/com/jme3/anim/JointTrack.java +++ b/jme3-core/src/main/java/com/jme3/anim/JointTrack.java @@ -55,7 +55,7 @@ public final class JointTrack extends TransformTrack implements JmeCloneable, Sa } /** - * Creates a bone track for the given bone index + * Creates a joint track for the given joint index * * @param target The Joint target of this track * @param times a float array with the time of each frame diff --git a/jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java b/jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java new file mode 100644 index 000000000..b17235099 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java @@ -0,0 +1,114 @@ +/* + * 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.anim; + +import com.jme3.export.*; +import com.jme3.math.*; +import com.jme3.scene.Spatial; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; + +/** + * Contains a list of transforms and times for each keyframe. + * + * @author Rémy Bouquet + */ +public final class SpatialTrack extends TransformTrack implements JmeCloneable, Savable { + + private Spatial target; + + /** + * Serialization-only. Do not use. + */ + public SpatialTrack() { + super(); + } + + /** + * Creates a spatial track for the given Spatial + * + * @param target The Spatial target of this track + * @param times a float array with the time of each frame + * @param translations the translation of the bone for each frame + * @param rotations the rotation of the bone for each frame + * @param scales the scale of the bone for each frame + */ + public SpatialTrack(Spatial target, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { + super(times, translations, rotations, scales); + this.target = target; + } + + @Override + public boolean interpolate(double t) { + setDefaultTransform(target.getLocalTransform()); + boolean running = super.interpolate(t); + Transform transform = getInterpolatedTransform(); + target.setLocalTransform(transform); + return running; + } + + public void setTarget(Spatial target) { + this.target = target; + } + + @Override + public Object jmeClone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException("Error cloning", e); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + super.cloneFields(cloner, original); + this.target = cloner.clone(target); + } + + @Override + public void write(JmeExporter ex) throws IOException { + super.write(ex); + OutputCapsule oc = ex.getCapsule(this); + oc.write(target, "target", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule ic = im.getCapsule(this); + target = (Spatial) ic.readSavable("target", null); + } + +} diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index 1623977c1..66e06e13e 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -31,7 +31,8 @@ */ package jme3test.model; -import com.jme3.animation.*; +import com.jme3.anim.AnimComposer; +import com.jme3.anim.SkinningControl; import com.jme3.app.ChaseCameraAppState; import com.jme3.app.SimpleApplication; import com.jme3.asset.plugins.FileLocator; @@ -43,11 +44,10 @@ import com.jme3.renderer.Limits; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.scene.control.Control; +import com.jme3.scene.debug.custom.ArmatureDebugAppState; +import com.jme3.scene.plugins.gltf.GltfModelKey; -import java.util.ArrayList; -import java.util.List; - -//import com.jme3.scene.debug.custom.SkeletonDebugAppState; +import java.util.*; public class TestGltfLoading extends SimpleApplication { @@ -75,8 +75,8 @@ public class TestGltfLoading extends SimpleApplication { */ public void simpleInitApp() { -// SkeletonDebugAppState skeletonDebugAppState = new SkeletonDebugAppState(); -// getStateManager().attach(skeletonDebugAppState); + ArmatureDebugAppState armatureDebugappState = new ArmatureDebugAppState(); + getStateManager().attach(armatureDebugappState); String folder = System.getProperty("user.home"); assetManager.registerLocator(folder, FileLocator.class); @@ -109,28 +109,40 @@ public class TestGltfLoading extends SimpleApplication { // rootNode.addLight(pl); // PointLight pl1 = new PointLight(new Vector3f(-5.0f, -5.0f, -5.0f), ColorRGBA.White.mult(0.5f), 50); // rootNode.addLight(pl1); - + //loadModel("Models/gltf/nier/scene.gltf", new Vector3f(0, -1.5f, 0), 0.01f); + //loadModel("Models/gltf/izzy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + //loadModel("Models/gltf/darth/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + //loadModel("Models/gltf/mech/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + //loadModel("Models/gltf/elephant/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + //loadModel("Models/gltf/buffalo/scene.gltf", new Vector3f(0, -1, 0), 0.1f); + //loadModel("Models/gltf/war/scene.gltf", new Vector3f(0, -1, 0), 0.1f); + //loadModel("Models/gltf/ganjaarl/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + //loadModel("Models/gltf/hero/scene.gltf", new Vector3f(0, -1, 0), 0.1f); + //loadModel("Models/gltf/mercy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + //loadModel("Models/gltf/crab/scene.gltf", Vector3f.ZERO, 1); + //loadModel("Models/gltf/manta/scene.gltf", Vector3f.ZERO, 0.2f); +// loadModel("Models/gltf/bone/scene.gltf", Vector3f.ZERO, 0.1f); // loadModel("Models/gltf/box/box.gltf", Vector3f.ZERO, 1); // loadModel("Models/gltf/duck/Duck.gltf", new Vector3f(0, -1, 0), 1); // loadModel("Models/gltf/damagedHelmet/damagedHelmet.gltf", Vector3f.ZERO, 1); - loadModel("Models/gltf/hornet/scene.gltf", new Vector3f(0, -0.5f, 0), 0.4f); +// loadModel("Models/gltf/hornet/scene.gltf", new Vector3f(0, -0.5f, 0), 0.4f); //// loadModel("Models/gltf/adamHead/adamHead.gltf", Vector3f.ZERO, 0.6f); - // loadModel("Models/gltf/busterDrone/busterDrone.gltf", new Vector3f(0, 0f, 0), 0.8f); + //loadModel("Models/gltf/busterDrone/busterDrone.gltf", new Vector3f(0, 0f, 0), 0.8f); // loadModel("Models/gltf/animatedCube/AnimatedCube.gltf", Vector3f.ZERO, 0.5f); // -// //loadModel("Models/gltf/BoxAnimated/BoxAnimated.gltf", new Vector3f(0, 0f, 0), 0.8f); + //loadModel("Models/gltf/BoxAnimated/BoxAnimated.gltf", new Vector3f(0, 0f, 0), 0.8f); // -// loadModel("Models/gltf/RiggedFigure/RiggedSimple.gltf", new Vector3f(0, -0.3f, 0), 0.2f); + //loadModel("Models/gltf/RiggedFigure/RiggedSimple.gltf", new Vector3f(0, -0.3f, 0), 0.2f); //loadModel("Models/gltf/RiggedFigure/RiggedFigure.gltf", new Vector3f(0, -1f, 0), 1f); //loadModel("Models/gltf/CesiumMan/CesiumMan.gltf", new Vector3f(0, -1, 0), 1f); //loadModel("Models/gltf/BrainStem/BrainStem.gltf", new Vector3f(0, -1, 0), 1f); //loadModel("Models/gltf/Jaime/Jaime.gltf", new Vector3f(0, -1, 0), 1f); -// loadModel("Models/gltf/GiantWorm/GiantWorm.gltf", new Vector3f(0, -1, 0), 1f); -// //loadModel("Models/gltf/RiggedFigure/WalkingLady.gltf", new Vector3f(0, -0.f, 0), 1f); -// loadModel("Models/gltf/Monster/Monster.gltf", Vector3f.ZERO, 0.03f); + // loadModel("Models/gltf/GiantWorm/GiantWorm.gltf", new Vector3f(0, -1, 0), 1f); + //loadModel("Models/gltf/RiggedFigure/WalkingLady.gltf", new Vector3f(0, -0.f, 0), 1f); + //loadModel("Models/gltf/Monster/Monster.gltf", Vector3f.ZERO, 0.03f); // loadModel("Models/gltf/corset/Corset.gltf", new Vector3f(0, -1, 0), 20f); - loadModel("Models/gltf/boxInter/BoxInterleaved.gltf", new Vector3f(0, 0, 0), 1f); + // loadModel("Models/gltf/boxInter/BoxInterleaved.gltf", new Vector3f(0, 0, 0), 1f); probeNode.attachChild(assets.get(0)); @@ -157,6 +169,7 @@ public class TestGltfLoading extends SimpleApplication { }, "autorotate"); inputManager.addMapping("toggleAnim", new KeyTrigger(KeyInput.KEY_RETURN)); + inputManager.addListener(new ActionListener() { @Override public void onAction(String name, boolean isPressed, float tpf) { @@ -170,6 +183,17 @@ public class TestGltfLoading extends SimpleApplication { } } }, "toggleAnim"); + inputManager.addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_RIGHT)); + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed && composer != null) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAnimClip(anim); + } + } + }, "nextAnim"); dumpScene(rootNode, 0); } @@ -192,7 +216,9 @@ public class TestGltfLoading extends SimpleApplication { } private void loadModel(String path, Vector3f offset, float scale) { - Spatial s = assetManager.loadModel(path); + GltfModelKey k = new GltfModelKey(path); + //k.setKeepSkeletonPose(true); + Spatial s = assetManager.loadModel(k); s.scale(scale); s.move(offset); assets.add(s); @@ -200,14 +226,15 @@ public class TestGltfLoading extends SimpleApplication { playFirstAnim(s); } - SkeletonControl ctrl = findControl(s, SkeletonControl.class); + SkinningControl ctrl = findControl(s, SkinningControl.class); -// //ctrl.getSpatial().removeControl(ctrl); + // ctrl.getSpatial().removeControl(ctrl); if (ctrl == null) { return; } - ctrl.setHardwareSkinningPreferred(false); - //getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(ctrl, true); + //System.err.println(ctrl.getArmature().toString()); + //ctrl.setHardwareSkinningPreferred(false); + getStateManager().getState(ArmatureDebugAppState.class).addArmatureFrom(ctrl); // AnimControl aCtrl = findControl(s, AnimControl.class); // //ctrl.getSpatial().removeControl(ctrl); // if (aCtrl == null) { @@ -219,17 +246,24 @@ public class TestGltfLoading extends SimpleApplication { } + Queue anims = new LinkedList<>(); + AnimComposer composer; + private void playFirstAnim(Spatial s) { - AnimControl control = s.getControl(AnimControl.class); + AnimComposer control = s.getControl(AnimComposer.class); if (control != null) { -// if (control.getAnimationNames().size() > 0) { -// control.createChannel().setAnim(control.getAnimationNames().iterator().next()); -// } - for (String name : control.getAnimationNames()) { - control.createChannel().setAnim(name); + anims.clear(); + for (String name : control.getAnimClipsNames()) { + anims.add(name); } - + if (anims.isEmpty()) { + return; + } + String anim = anims.poll(); + anims.add(anim); + control.setCurrentAnimClip(anim); + composer = control; } if (s instanceof Node) { Node n = (Node) s; @@ -241,17 +275,9 @@ public class TestGltfLoading extends SimpleApplication { private void stopAnim(Spatial s) { - AnimControl control = s.getControl(AnimControl.class); + AnimComposer control = s.getControl(AnimComposer.class); if (control != null) { - for (int i = 0; i < control.getNumChannels(); i++) { - AnimChannel ch = control.getChannel(i); - ch.reset(true); - } - control.clearChannels(); - if (control.getSkeleton() != null) { - control.getSkeleton().reset(); - } - + control.reset(); } if (s instanceof Node) { Node n = (Node) s; diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading2.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading2.java deleted file mode 100644 index d467fbc9b..000000000 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading2.java +++ /dev/null @@ -1,320 +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 jme3test.model; - -import com.jme3.animation.*; -import com.jme3.app.ChaseCameraAppState; -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.math.*; -import com.jme3.renderer.Limits; -import com.jme3.scene.Node; -import com.jme3.scene.Spatial; -import com.jme3.scene.control.Control; -import com.jme3.scene.plugins.gltf.GltfModelKey; - -import java.util.*; - -//import com.jme3.scene.debug.custom.SkeletonDebugAppState; - -public class TestGltfLoading2 extends SimpleApplication { - - Node autoRotate = new Node("autoRotate"); - List assets = new ArrayList<>(); - Node probeNode; - float time = 0; - int assetIndex = 0; - boolean useAutoRotate = false; - private final static String indentString = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"; - int duration = 2; - boolean playAnim = true; - - public static void main(String[] args) { - TestGltfLoading2 app = new TestGltfLoading2(); - app.start(); - } - - /* - WARNING this test case can't wok without the assets, and considering their size, they are not pushed into the repo - you can find them here : - https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0 - https://sketchfab.com/features/gltf - You have to copy them in Model/gltf folder in the test-data project. - */ - public void simpleInitApp() { - -// SkeletonDebugAppState skeletonDebugAppState = new SkeletonDebugAppState(); -// getStateManager().attach(skeletonDebugAppState); - - // cam.setLocation(new Vector3f(4.0339394f, 2.645184f, 6.4627485f)); - // cam.setRotation(new Quaternion(-0.013950467f, 0.98604023f, -0.119502485f, -0.11510504f)); - cam.setFrustumPerspective(45f, (float) cam.getWidth() / cam.getHeight(), 0.1f, 100f); - renderer.setDefaultAnisotropicFilter(Math.min(renderer.getLimits().get(Limits.TextureAnisotropy), 8)); - setPauseOnLostFocus(false); - - flyCam.setMoveSpeed(5); - flyCam.setDragToRotate(true); - flyCam.setEnabled(false); - viewPort.setBackgroundColor(new ColorRGBA().setAsSrgb(0.2f, 0.2f, 0.2f, 1.0f)); - rootNode.attachChild(autoRotate); - probeNode = (Node) assetManager.loadModel("Scenes/defaultProbe.j3o"); - autoRotate.attachChild(probeNode); - -// DirectionalLight dl = new DirectionalLight(); -// dl.setDirection(new Vector3f(-1f, -1.0f, -1f).normalizeLocal()); -// dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f)); -// rootNode.addLight(dl); - -// DirectionalLight dl2 = new DirectionalLight(); -// dl2.setDirection(new Vector3f(1f, 1.0f, 1f).normalizeLocal()); -// dl2.setColor(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f)); -// rootNode.addLight(dl2); - -// PointLight pl = new PointLight(new Vector3f(5.0f, 5.0f, 5.0f), ColorRGBA.White, 30); -// rootNode.addLight(pl); -// PointLight pl1 = new PointLight(new Vector3f(-5.0f, -5.0f, -5.0f), ColorRGBA.White.mult(0.5f), 50); -// rootNode.addLight(pl1); - //loadModel("Models/gltf/buffalo/scene.gltf", new Vector3f(0, -1, 0), 0.1f); - //loadModel("Models/gltf/war/scene.gltf", new Vector3f(0, -1, 0), 0.1f); - loadModel("Models/gltf/ganjaarl/scene.gltf", new Vector3f(0, -1, 0), 0.01f); - //loadModel("Models/gltf/hero/scene.gltf", new Vector3f(0, -1, 0), 0.1f); - //loadModel("Models/gltf/mercy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); - //loadModel("Models/gltf/crab/scene.gltf", Vector3f.ZERO, 1); - //loadModel("Models/gltf/manta/scene.gltf", Vector3f.ZERO, 0.2f); -// loadModel("Models/gltf/bone/scene.gltf", Vector3f.ZERO, 0.1f); -// loadModel("Models/gltf/box/box.gltf", Vector3f.ZERO, 1); -// loadModel("Models/gltf/duck/Duck.gltf", new Vector3f(0, -1, 0), 1); -// loadModel("Models/gltf/damagedHelmet/damagedHelmet.gltf", Vector3f.ZERO, 1); -// loadModel("Models/gltf/hornet/scene.gltf", new Vector3f(0, -0.5f, 0), 0.4f); -//// loadModel("Models/gltf/adamHead/adamHead.gltf", Vector3f.ZERO, 0.6f); - //loadModel("Models/gltf/busterDrone/busterDrone.gltf", new Vector3f(0, 0f, 0), 0.8f); -// loadModel("Models/gltf/animatedCube/AnimatedCube.gltf", Vector3f.ZERO, 0.5f); -// -// //loadModel("Models/gltf/BoxAnimated/BoxAnimated.gltf", new Vector3f(0, 0f, 0), 0.8f); -// - //loadModel("Models/gltf/RiggedFigure/RiggedSimple.gltf", new Vector3f(0, -0.3f, 0), 0.2f); - //loadModel("Models/gltf/RiggedFigure/RiggedFigure.gltf", new Vector3f(0, -1f, 0), 1f); - //loadModel("Models/gltf/CesiumMan/CesiumMan.gltf", new Vector3f(0, -1, 0), 1f); - //loadModel("Models/gltf/BrainStem/BrainStem.gltf", new Vector3f(0, -1, 0), 1f); - //loadModel("Models/gltf/Jaime/Jaime.gltf", new Vector3f(0, -1, 0), 1f); - //loadModel("Models/gltf/GiantWorm/GiantWorm.gltf", new Vector3f(0, -1, 0), 1f); - //loadModel("Models/gltf/RiggedFigure/WalkingLady.gltf", new Vector3f(0, -0.f, 0), 1f); - //loadModel("Models/gltf/Monster/Monster.gltf", Vector3f.ZERO, 0.03f); - -// loadModel("Models/gltf/corset/Corset.gltf", new Vector3f(0, -1, 0), 20f); - // loadModel("Models/gltf/boxInter/BoxInterleaved.gltf", new Vector3f(0, 0, 0), 1f); - - - probeNode.attachChild(assets.get(0)); - - ChaseCameraAppState chaseCam = new ChaseCameraAppState(); - chaseCam.setTarget(probeNode); - getStateManager().attach(chaseCam); - chaseCam.setInvertHorizontalAxis(true); - chaseCam.setInvertVerticalAxis(true); - chaseCam.setZoomSpeed(0.5f); - chaseCam.setMinVerticalRotation(-FastMath.HALF_PI); - chaseCam.setRotationSpeed(3); - chaseCam.setDefaultDistance(3); - chaseCam.setDefaultVerticalRotation(0.3f); - - inputManager.addMapping("autorotate", new KeyTrigger(KeyInput.KEY_SPACE)); - inputManager.addListener(new ActionListener() { - @Override - public void onAction(String name, boolean isPressed, float tpf) { - if (isPressed) { - useAutoRotate = !useAutoRotate; - } - } - }, "autorotate"); - - inputManager.addMapping("toggleAnim", new KeyTrigger(KeyInput.KEY_RETURN)); - - inputManager.addListener(new ActionListener() { - @Override - public void onAction(String name, boolean isPressed, float tpf) { - if (isPressed) { - playAnim = !playAnim; - if (playAnim) { - playFirstAnim(rootNode); - } else { - stopAnim(rootNode); - } - } - } - }, "toggleAnim"); - inputManager.addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_RIGHT)); - inputManager.addListener(new ActionListener() { - @Override - public void onAction(String name, boolean isPressed, float tpf) { - if (isPressed && animControl != null) { - AnimChannel c = animControl.getChannel(0); - if (c == null) { - c = animControl.createChannel(); - } - String anim = anims.poll(); - anims.add(anim); - c.setAnim(anim); - } - } - }, "nextAnim"); - - dumpScene(rootNode, 0); - } - - private T findControl(Spatial s, Class controlClass) { - T ctrl = s.getControl(controlClass); - if (ctrl != null) { - return ctrl; - } - if (s instanceof Node) { - Node n = (Node) s; - for (Spatial spatial : n.getChildren()) { - ctrl = findControl(spatial, controlClass); - if (ctrl != null) { - return ctrl; - } - } - } - return null; - } - - private void loadModel(String path, Vector3f offset, float scale) { - GltfModelKey k = new GltfModelKey(path); - //k.setKeepSkeletonPose(true); - Spatial s = assetManager.loadModel(k); - s.scale(scale); - s.move(offset); - assets.add(s); - if (playAnim) { - playFirstAnim(s); - } - - SkeletonControl ctrl = findControl(s, SkeletonControl.class); - - // ctrl.getSpatial().removeControl(ctrl); - if (ctrl == null) { - return; - } - //System.err.println(ctrl.getArmature().toString()); - //ctrl.setHardwareSkinningPreferred(false); - // getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(ctrl, true); -// AnimControl aCtrl = findControl(s, AnimControl.class); -// //ctrl.getSpatial().removeControl(ctrl); -// if (aCtrl == null) { -// return; -// } -// if (aCtrl.getArmature() != null) { -// getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(aCtrl.getArmature(), aCtrl.getSpatial(), true); -// } - - } - - Queue anims = new LinkedList<>(); - AnimControl animControl; - - private void playFirstAnim(Spatial s) { - - AnimControl control = s.getControl(AnimControl.class); - if (control != null) { - anims.clear(); - for (String name : control.getAnimationNames()) { - anims.add(name); - } - if (anims.isEmpty()) { - return; - } - String anim = anims.poll(); - anims.add(anim); - control.createChannel().setAnim(anim); - animControl = control; - } - if (s instanceof Node) { - Node n = (Node) s; - for (Spatial spatial : n.getChildren()) { - playFirstAnim(spatial); - } - } - } - - private void stopAnim(Spatial s) { - - AnimControl control = s.getControl(AnimControl.class); - if (control != null) { - for (int i = 0; i < control.getNumChannels(); i++) { - AnimChannel ch = control.getChannel(i); - ch.reset(true); - } - control.clearChannels(); - } - if (s instanceof Node) { - Node n = (Node) s; - for (Spatial spatial : n.getChildren()) { - stopAnim(spatial); - } - } - } - - @Override - public void simpleUpdate(float tpf) { - - if (!useAutoRotate) { - return; - } - time += tpf; - autoRotate.rotate(0, tpf * 0.5f, 0); - if (time > duration) { - assets.get(assetIndex).removeFromParent(); - assetIndex = (assetIndex + 1) % assets.size(); - if (assetIndex == 0) { - duration = 10; - } - probeNode.attachChild(assets.get(assetIndex)); - time = 0; - } - } - - private void dumpScene(Spatial s, int indent) { - System.err.println(indentString.substring(0, indent) + s.getName() + " (" + s.getClass().getSimpleName() + ") / " + - s.getLocalTransform().getTranslation().toString() + ", " + - s.getLocalTransform().getRotation().toString() + ", " + - s.getLocalTransform().getScale().toString()); - if (s instanceof Node) { - Node n = (Node) s; - for (Spatial spatial : n.getChildren()) { - dumpScene(spatial, indent + 1); - } - } - } -} 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 0b1350845..a2bc44aec 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 @@ -2,7 +2,7 @@ package com.jme3.scene.plugins.gltf; import com.google.gson.*; import com.google.gson.stream.JsonReader; -import com.jme3.animation.*; +import com.jme3.anim.*; import com.jme3.asset.*; import com.jme3.material.Material; import com.jme3.material.RenderState; @@ -196,7 +196,7 @@ public class GltfLoader implements AssetLoader { public Object readNode(int nodeIndex) throws IOException { Object obj = fetchFromCache("nodes", nodeIndex, Object.class); if (obj != null) { - if (obj instanceof BoneWrapper) { + if (obj instanceof JointWrapper) { //the node can be a previously loaded bone let's return it return obj; } else { @@ -274,9 +274,9 @@ public class GltfLoader implements AssetLoader { readChild(spatial, child); } } - } else if (loaded instanceof BoneWrapper) { + } else if (loaded instanceof JointWrapper) { //parent is the Armature Node, we have to apply its transforms to the root bone's animation data - BoneWrapper bw = (BoneWrapper) loaded; + JointWrapper bw = (JointWrapper) loaded; bw.isRoot = true; SkinData skinData = fetchFromCache("skins", bw.skinIndex, SkinData.class); if (skinData == null) { @@ -792,44 +792,37 @@ public class GltfLoader implements AssetLoader { } List spatials = new ArrayList<>(); - Animation anim = new Animation(); - anim.setName(name); + AnimClip anim = new AnimClip(name); int skinIndex = -1; - List usedBones = new ArrayList<>(); + List usedJoints = new ArrayList<>(); for (int i = 0; i < tracks.length; i++) { TrackData trackData = tracks[i]; if (trackData == null || trackData.timeArrays.isEmpty()) { continue; } trackData.update(); - if (trackData.length > anim.getLength()) { - anim.setLength(trackData.length); - } Object node = fetchFromCache("nodes", i, Object.class); if (node instanceof Spatial) { Spatial s = (Spatial) node; spatials.add(s); - SpatialTrack track = new SpatialTrack(trackData.times, trackData.translations, trackData.rotations, trackData.scales); - track.setTrackSpatial(s); + SpatialTrack track = new SpatialTrack(s, trackData.times, trackData.translations, trackData.rotations, trackData.scales); anim.addTrack(track); - } else if (node instanceof BoneWrapper) { - BoneWrapper b = (BoneWrapper) node; - //apply the inverseBindMatrix to animation data. - b.update(trackData); - usedBones.add(b.bone); + } else if (node instanceof JointWrapper) { + JointWrapper jw = (JointWrapper) node; + usedJoints.add(jw.joint); if (skinIndex == -1) { - skinIndex = b.skinIndex; + skinIndex = jw.skinIndex; } else { - //Check if all bones affected by this animation are from the same skin, the track will be skipped. - if (skinIndex != b.skinIndex) { - logger.log(Level.WARNING, "Animation " + animationIndex + " (" + name + ") applies to bones that are not from the same skin: skin " + skinIndex + ", bone " + b.bone.getName() + " from skin " + b.skinIndex); + //Check if all joints affected by this animation are from the same skin, the track will be skipped. + if (skinIndex != jw.skinIndex) { + logger.log(Level.WARNING, "Animation " + animationIndex + " (" + name + ") applies to joints that are not from the same skin: skin " + skinIndex + ", joint " + jw.joint.getName() + " from skin " + jw.skinIndex); continue; } } - BoneTrack track = new BoneTrack(b.boneIndex, trackData.times, trackData.translations, trackData.rotations, trackData.scales); + JointTrack track = new JointTrack(jw.joint, trackData.times, trackData.translations, trackData.rotations, trackData.scales); anim.addTrack(track); } @@ -840,22 +833,15 @@ public class GltfLoader implements AssetLoader { // instead of the local pose that is supposed to be the default if (skinIndex != -1) { SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class); - Skeleton skeleton = skin.skeletonControl.getSkeleton(); - for (Bone bone : skin.bones) { - if (!usedBones.contains(bone) && !equalBindAndLocalTransforms(bone)) { + for (Joint joint : skin.joints) { + if (!usedJoints.contains(joint)) {// && !equalBindAndLocalTransforms(joint) //create a track - float[] times = new float[]{0, anim.getLength()}; - - Vector3f t = bone.getLocalPosition().subtract(bone.getBindPosition()); - Quaternion r = tmpQuat.set(bone.getBindRotation()).inverse().multLocal(bone.getLocalRotation()); - Vector3f s = bone.getLocalScale().divide(bone.getBindScale()); + float[] times = new float[]{0}; - Vector3f[] translations = new Vector3f[]{t, t}; - Quaternion[] rotations = new Quaternion[]{r, r}; - Vector3f[] scales = new Vector3f[]{s, s}; - - int boneIndex = skeleton.getBoneIndex(bone); - BoneTrack track = new BoneTrack(boneIndex, times, translations, rotations, scales); + Vector3f[] translations = new Vector3f[]{joint.getLocalTranslation()}; + Quaternion[] rotations = new Quaternion[]{joint.getLocalRotation()}; + Vector3f[] scales = new Vector3f[]{joint.getLocalScale()}; + JointTrack track = new JointTrack(joint, times, translations, rotations, scales); anim.addTrack(track); } } @@ -864,9 +850,9 @@ public class GltfLoader implements AssetLoader { anim = customContentManager.readExtensionAndExtras("animations", animation, anim); if (skinIndex != -1) { - //we have a bone animation. + //we have a armature animation. SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class); - skin.animControl.addAnim(anim); + skin.animComposer.addAnimClip(anim); } if (!spatials.isEmpty()) { @@ -885,12 +871,12 @@ public class GltfLoader implements AssetLoader { spatial = findCommonAncestor(spatials); } - AnimControl control = spatial.getControl(AnimControl.class); - if (control == null) { - control = new AnimControl(); - spatial.addControl(control); + AnimComposer composer = spatial.getControl(AnimComposer.class); + if (composer == null) { + composer = new AnimComposer(); + spatial.addControl(composer); } - control.addAnim(anim); + composer.addAnimClip(anim); } } } @@ -932,16 +918,16 @@ public class GltfLoader implements AssetLoader { //It's not mandatory and exporters tends to mix up how it should be used because the specs are not clear. //Anyway we have other means to detect both armature structures and root bones. - JsonArray joints = skin.getAsJsonArray("joints"); - assertNotNull(joints, "No joints defined for skin"); - int idx = allJoints.indexOf(joints); + JsonArray jsonJoints = skin.getAsJsonArray("joints"); + assertNotNull(jsonJoints, "No joints defined for skin"); + int idx = allJoints.indexOf(jsonJoints); if (idx >= 0) { //skin already exists let's just set it in the cache SkinData sd = fetchFromCache("skins", idx, SkinData.class); addToCache("skins", index, sd, nodes.size()); continue; } else { - allJoints.add(joints); + allJoints.add(jsonJoints); } //These inverse bind matrices, once inverted again, will give us the real bind pose of the bones (in model space), @@ -951,136 +937,75 @@ public class GltfLoader implements AssetLoader { if (matricesIndex != null) { inverseBindMatrices = readAccessorData(matricesIndex, matrix4fArrayPopulator); } else { - inverseBindMatrices = new Matrix4f[joints.size()]; + inverseBindMatrices = new Matrix4f[jsonJoints.size()]; for (int i = 0; i < inverseBindMatrices.length; i++) { inverseBindMatrices[i] = new Matrix4f(); } } - Bone[] bones = new Bone[joints.size()]; - for (int i = 0; i < joints.size(); i++) { - int boneIndex = joints.get(i).getAsInt(); - //we don't need the inverse bind matrix, we need the bind matrix so let's invert it. - Matrix4f modelBindMatrix = inverseBindMatrices[i].invertLocal(); - bones[i] = readNodeAsBone(boneIndex, i, index, modelBindMatrix); + Joint[] joints = new Joint[jsonJoints.size()]; + for (int i = 0; i < jsonJoints.size(); i++) { + int boneIndex = jsonJoints.get(i).getAsInt(); + Matrix4f inverseModelBindMatrix = inverseBindMatrices[i]; + joints[i] = readNodeAsBone(boneIndex, i, index, inverseModelBindMatrix); } - for (int i = 0; i < joints.size(); i++) { - findChildren(joints.get(i).getAsInt()); + for (int i = 0; i < jsonJoints.size(); i++) { + findChildren(jsonJoints.get(i).getAsInt()); } - Skeleton skeleton = new Skeleton(bones); + Armature armature = new Armature(joints); - //Compute bind transforms. We need to do it from root bone to leaves bone. - for (Bone bone : skeleton.getRoots()) { - BoneWrapper bw = findBoneWrapper(bone); - computeBindTransforms(bw, skeleton); - } - - skeleton = customContentManager.readExtensionAndExtras("skin", skin, skeleton); + armature = customContentManager.readExtensionAndExtras("skin", skin, armature); SkinData skinData = new SkinData(); - skinData.bones = bones; - skinData.skeletonControl = new SkeletonControl(skeleton); - skinData.animControl = new AnimControl(skinData.skeletonControl.getSkeleton()); + skinData.joints = joints; + skinData.skinningControl = new SkinningControl(armature); + skinData.animComposer = new AnimComposer(); addToCache("skins", index, skinData, nodes.size()); skinnedSpatials.put(skinData, new ArrayList()); - // Set local transforms. - // The skeleton may come in a given pose, that is not the rest pose, so let 's apply it. - // We will need it later for animation - for (int i = 0; i < joints.size(); i++) { - applyPose(joints.get(i).getAsInt()); - } - skeleton.updateWorldVectors(); - - //If the user didn't ask to keep the pose we reset the skeleton user control - if (!isKeepSkeletonPose(info)) { - for (Bone bone : bones) { - bone.setUserControl(false); - } - } + armature.update(); } } - private void applyPose(int index) { - BoneWrapper bw = fetchFromCache("nodes", index, BoneWrapper.class); - bw.bone.setUserControl(true); - bw.bone.setLocalTranslation(bw.localTransform.getTranslation()); - bw.bone.setLocalRotation(bw.localTransform.getRotation()); - bw.bone.setLocalScale(bw.localTransform.getScale()); - } + public Joint readNodeAsBone(int nodeIndex, int jointIndex, int skinIndex, Matrix4f inverseModelBindMatrix) throws IOException { - private void computeBindTransforms(BoneWrapper boneWrapper, Skeleton skeleton) { - Bone bone = boneWrapper.bone; - tmpTransforms.fromTransformMatrix(boneWrapper.modelBindMatrix); - if (bone.getParent() != null) { - //root bone, model transforms are the same as the local transforms - //but for child bones we need to combine it with the parents inverse model transforms. - tmpMat.setTranslation(bone.getParent().getModelSpacePosition()); - tmpMat.setRotationQuaternion(bone.getParent().getModelSpaceRotation()); - tmpMat.setScale(bone.getParent().getModelSpaceScale()); - tmpMat.invertLocal(); - tmpTransforms2.fromTransformMatrix(tmpMat); - tmpTransforms.combineWithParent(tmpTransforms2); - } - bone.setBindTransforms(tmpTransforms.getTranslation(), tmpTransforms.getRotation(), tmpTransforms.getScale()); - - //resets the local transforms to bind transforms for all bones. - //then computes the model transforms from local transforms for each bone. - skeleton.resetAndUpdate(); - skeleton.setBindingPose(); - for (Integer childIndex : boneWrapper.children) { - BoneWrapper child = fetchFromCache("nodes", childIndex, BoneWrapper.class); - computeBindTransforms(child, skeleton); - } - } - - private BoneWrapper findBoneWrapper(Bone bone) { - for (int i = 0; i < nodes.size(); i++) { - BoneWrapper bw = fetchFromCache("nodes", i, BoneWrapper.class); - if (bw != null && bw.bone == bone) { - return bw; - } - } - return null; - } - - public Bone readNodeAsBone(int nodeIndex, int boneIndex, int skinIndex, Matrix4f modelBindMatrix) throws IOException { - - BoneWrapper boneWrapper = fetchFromCache("nodes", nodeIndex, BoneWrapper.class); - if (boneWrapper != null) { - return boneWrapper.bone; + JointWrapper jointWrapper = fetchFromCache("nodes", nodeIndex, JointWrapper.class); + if (jointWrapper != null) { + return jointWrapper.joint; } JsonObject nodeData = nodes.get(nodeIndex).getAsJsonObject(); String name = getAsString(nodeData, "name"); if (name == null) { - name = "Bone_" + nodeIndex; + name = "Joint_" + nodeIndex; } - Bone bone = new Bone(name); + Joint joint = new Joint(name); Transform boneTransforms = null; boneTransforms = readTransforms(nodeData); + joint.setLocalTransform(boneTransforms); + joint.setInverseModelBindMatrix(inverseModelBindMatrix); - addToCache("nodes", nodeIndex, new BoneWrapper(bone, boneIndex, skinIndex, modelBindMatrix, boneTransforms), nodes.size()); + addToCache("nodes", nodeIndex, new JointWrapper(joint, jointIndex, skinIndex), nodes.size()); - return bone; + return joint; } private void findChildren(int nodeIndex) throws IOException { - BoneWrapper bw = fetchFromCache("nodes", nodeIndex, BoneWrapper.class); + JointWrapper jw = fetchFromCache("nodes", nodeIndex, JointWrapper.class); JsonObject nodeData = nodes.get(nodeIndex).getAsJsonObject(); JsonArray children = nodeData.getAsJsonArray("children"); if (children != null) { for (JsonElement child : children) { int childIndex = child.getAsInt(); - if (bw.children.contains(childIndex)) { + if (jw.children.contains(childIndex)) { //bone already has the child in its children continue; } - BoneWrapper cbw = fetchFromCache("nodes", childIndex, BoneWrapper.class); - if (cbw != null) { - bw.bone.addChild(cbw.bone); - bw.children.add(childIndex); + JointWrapper cjw = fetchFromCache("nodes", childIndex, JointWrapper.class); + if (cjw != null) { + jw.joint.addChild(cjw.joint); + jw.children.add(childIndex); } else { //The child might be a Node //Creating a dummy node to read the subgraph @@ -1089,7 +1014,7 @@ public class GltfLoader implements AssetLoader { Spatial s = n.getChild(0); //removing the spatial from the dummy node, it will be attached to the attachment node of the bone s.removeFromParent(); - bw.attachedSpatial = s; + jw.attachedSpatial = s; } } @@ -1113,19 +1038,19 @@ public class GltfLoader implements AssetLoader { skinData.rootBoneTransformOffset.combineWithParent(skinData.parent.getWorldTransform()); } - if (skinData.animControl != null && skinData.animControl.getSpatial() == null) { - spatial.addControl(skinData.animControl); + if (skinData.animComposer != null && skinData.animComposer.getSpatial() == null) { + spatial.addControl(skinData.animComposer); } - spatial.addControl(skinData.skeletonControl); + spatial.addControl(skinData.skinningControl); } for (int i = 0; i < nodes.size(); i++) { - BoneWrapper bw = fetchFromCache("nodes", i, BoneWrapper.class); + JointWrapper bw = fetchFromCache("nodes", i, JointWrapper.class); if (bw == null || bw.attachedSpatial == null) { continue; } SkinData skinData = fetchFromCache("skins", bw.skinIndex, SkinData.class); - skinData.skeletonControl.getAttachmentsNode(bw.bone.getName()).attachChild(bw.attachedSpatial); + skinData.skinningControl.getAttachmentsNode(bw.joint.getName()).attachChild(bw.attachedSpatial); } } @@ -1182,118 +1107,27 @@ public class GltfLoader implements AssetLoader { } - private class BoneWrapper { - Bone bone; - int boneIndex; + private class JointWrapper { + Joint joint; + int jointIndex; int skinIndex; - Transform localTransform; - Transform localTransformOffset; - Matrix4f modelBindMatrix; boolean isRoot = false; - boolean localUpdated = false; Spatial attachedSpatial; List children = new ArrayList<>(); - public BoneWrapper(Bone bone, int boneIndex, int skinIndex, Matrix4f modelBindMatrix, Transform localTransform) { - this.bone = bone; - this.boneIndex = boneIndex; + public JointWrapper(Joint joint, int jointIndex, int skinIndex) { + this.joint = joint; + this.jointIndex = jointIndex; this.skinIndex = skinIndex; - this.modelBindMatrix = modelBindMatrix; - this.localTransform = localTransform; - this.localTransformOffset = localTransform.clone(); - } - - /** - * Applies the inverse Bind transforms to anim data. and the armature transforms if relevant. - */ - public void update(TrackData data) { - Transform bindTransforms = new Transform(bone.getBindPosition(), bone.getBindRotation(), bone.getBindScale()); - SkinData skinData = fetchFromCache("skins", skinIndex, SkinData.class); - - if (!localUpdated) { - //LocalTransform of the bone are default position to use for animations when there is no track. - //We need to transform them so that JME can us them in blendAnimTransform. - reverseBlendAnimTransforms(localTransformOffset, bindTransforms); - localUpdated = true; - } - - for (int i = 0; i < data.getNbKeyFrames(); i++) { - - Vector3f translation = getTranslation(data, i); - Quaternion rotation = getRotation(data, i); - Vector3f scale = getScale(data, i); - - Transform t = new Transform(translation, rotation, scale); - if (isRoot && skinData.rootBoneTransformOffset != null) { - //Apply the armature transforms to the root bone anim track. - t.combineWithParent(skinData.rootBoneTransformOffset); - } - - reverseBlendAnimTransforms(t, bindTransforms); - - if (data.translations != null) { - data.translations[i] = t.getTranslation(); - } - if (data.rotations != null) { - data.rotations[i] = t.getRotation(); - } - if (data.scales != null) { - data.scales[i] = t.getScale(); - } - } - - data.ensureTranslationRotations(localTransformOffset); - } - - private void reverseBlendAnimTransforms(Transform t, Transform bindTransforms) { - //This is wrong - //You'd normally combine those transforms with transform.combineWithParent() - //Here we actually do in reverse what JME does to combine anim transforms with bind transfoms (add trans/mult rot/ mult scale) - //The code to fix is in Bone.blendAnimTransforms - //TODO fix blendAnimTransforms - t.getTranslation().subtractLocal(bindTransforms.getTranslation()); - t.getScale().divideLocal(bindTransforms.getScale()); - tmpQuat.set(bindTransforms.getRotation()).inverseLocal().multLocal(t.getRotation()); - t.setRotation(tmpQuat); - } - - private Vector3f getTranslation(TrackData data, int i) { - Vector3f translation; - if (data.translations == null) { - translation = bone.getLocalPosition(); - } else { - translation = data.translations[i]; - } - return translation; - } - - private Quaternion getRotation(TrackData data, int i) { - Quaternion rotation; - if (data.rotations == null) { - rotation = bone.getLocalRotation(); - } else { - rotation = data.rotations[i]; - } - return rotation; - } - - private Vector3f getScale(TrackData data, int i) { - Vector3f scale; - if (data.scales == null) { - scale = bone.getLocalScale(); - } else { - scale = data.scales[i]; - } - return scale; } } private class SkinData { - SkeletonControl skeletonControl; - AnimControl animControl; + SkinningControl skinningControl; + AnimComposer animComposer; Spatial parent; Transform rootBoneTransformOffset; - Bone[] bones; + Joint[] joints; boolean used = false; } 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 68675e680..e32c96a6d 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 @@ -1,7 +1,6 @@ package com.jme3.scene.plugins.gltf; import com.google.gson.*; -import com.jme3.animation.Bone; import com.jme3.asset.AssetInfo; import com.jme3.asset.AssetLoadException; import com.jme3.math.*; @@ -686,11 +685,11 @@ public class GltfUtils { } } - public static boolean equalBindAndLocalTransforms(Bone b) { - return equalsEpsilon(b.getBindPosition(), b.getLocalPosition()) - && equalsEpsilon(b.getBindRotation(), b.getLocalRotation()) - && equalsEpsilon(b.getBindScale(), b.getLocalScale()); - } +// public static boolean equalBindAndLocalTransforms(Joint b) { +// return equalsEpsilon(b.getBindPosition(), b.getLocalPosition()) +// && equalsEpsilon(b.getBindRotation(), b.getLocalRotation()) +// && equalsEpsilon(b.getBindScale(), b.getLocalScale()); +// } private static float epsilon = 0.0001f; From 054d54f4b92a8f7832cb91808037658eead1a569 Mon Sep 17 00:00:00 2001 From: Nehon Date: Mon, 25 Dec 2017 15:09:55 +0100 Subject: [PATCH 13/54] Added a way to toggle on/off the non deforming bones in the Armature debugger --- .../src/main/java/com/jme3/anim/Armature.java | 9 +++-- .../debug/custom/ArmatureDebugAppState.java | 38 +++++++++++++++++-- .../scene/debug/custom/ArmatureDebugger.java | 27 ++++++++++--- .../jme3/scene/debug/custom/ArmatureNode.java | 27 ++++++++----- .../model/anim/TestAnimMigration.java | 15 ++++++-- 5 files changed, 91 insertions(+), 25 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/anim/Armature.java b/jme3-core/src/main/java/com/jme3/anim/Armature.java index 370618e7f..98b6299be 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Armature.java +++ b/jme3-core/src/main/java/com/jme3/anim/Armature.java @@ -8,8 +8,7 @@ import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.*; /** * Created by Nehon on 15/12/2017. @@ -104,7 +103,7 @@ public class Armature implements JmeCloneable, Savable { } /** - * returns the array of all root joints of this Armatire + * returns the array of all root joints of this Armature * * @return */ @@ -112,6 +111,10 @@ public class Armature implements JmeCloneable, Savable { return rootJoints; } + public List getJointList() { + return Arrays.asList(jointList); + } + /** * return a joint for the given index * diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java index 47b78c1c1..10bef21ec 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java @@ -8,9 +8,9 @@ import com.jme3.anim.*; import com.jme3.app.Application; import com.jme3.app.state.BaseAppState; import com.jme3.collision.CollisionResults; +import com.jme3.input.KeyInput; import com.jme3.input.MouseInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.input.controls.*; import com.jme3.light.DirectionalLight; import com.jme3.math.*; import com.jme3.renderer.ViewPort; @@ -27,6 +27,7 @@ public class ArmatureDebugAppState extends BaseAppState { private Map armatures = new HashMap<>(); private Map selectedBones = new HashMap<>(); private Application app; + private boolean displayAllJoints = false; ViewPort vp; @Override @@ -38,8 +39,9 @@ public class ArmatureDebugAppState extends BaseAppState { for (ArmatureDebugger armatureDebugger : armatures.values()) { armatureDebugger.initialize(app.getAssetManager()); } - app.getInputManager().addListener(actionListener, "shoot"); + app.getInputManager().addListener(actionListener, "shoot", "toggleJoints"); app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); + app.getInputManager().addMapping("toggleJoints", new KeyTrigger(KeyInput.KEY_F10)); debugNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal())); @@ -76,7 +78,10 @@ public class ArmatureDebugAppState extends BaseAppState { public ArmatureDebugger addArmatureFrom(Armature armature, Spatial forSpatial) { - ArmatureDebugger ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature); + JointInfoVisitor visitor = new JointInfoVisitor(armature); + forSpatial.depthFirstTraversal(visitor); + + ArmatureDebugger ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature, visitor.deformingJoints); ad.setLocalTransform(forSpatial.getWorldTransform()); if (forSpatial instanceof Node) { List geoms = new ArrayList<>(); @@ -149,6 +154,12 @@ public class ArmatureDebugAppState extends BaseAppState { } } } + if (name.equals("toggleJoints") && isPressed) { + displayAllJoints = !displayAllJoints; + for (ArmatureDebugger ad : armatures.values()) { + ad.displayNonDeformingJoint(displayAllJoints); + } + } } }; @@ -163,4 +174,23 @@ public class ArmatureDebugAppState extends BaseAppState { public void setDebugNode(Node debugNode) { this.debugNode = debugNode; } + + private class JointInfoVisitor extends SceneGraphVisitorAdapter { + + List deformingJoints = new ArrayList<>(); + Armature armature; + + public JointInfoVisitor(Armature armature) { + this.armature = armature; + } + + @Override + public void visit(Geometry g) { + for (Joint joint : armature.getJointList()) { + if (g.getMesh().isAnimatedByJoint(armature.getJointIndex(joint))) { + deformingJoints.add(joint); + } + } + } + } } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java index 43a4e41d7..aec5f532e 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java @@ -60,9 +60,10 @@ public class ArmatureDebugger extends Node { private Armature armature; - Node joints; - Node outlines; - Node wires; + private Node joints; + private Node outlines; + private Node wires; + private List deformingJoints; /** * The dotted lines between a bone's tail and the had of its children. Not @@ -83,28 +84,42 @@ public class ArmatureDebugger extends Node { * @param name the name of the debugger's node * @param armature the armature that will be shown */ - public ArmatureDebugger(String name, Armature armature) { + public ArmatureDebugger(String name, Armature armature, List deformingJoints) { super(name); + this.deformingJoints = deformingJoints; this.armature = armature; armature.update(); - joints = new Node("joints"); outlines = new Node("outlines"); wires = new Node("bones"); this.attachChild(joints); this.attachChild(outlines); this.attachChild(wires); + Node ndJoints = new Node("non deforming Joints"); + Node ndOutlines = new Node("non deforming Joints outlines"); + Node ndWires = new Node("non deforming Joints wires"); + joints.attachChild(ndJoints); + outlines.attachChild(ndOutlines); + wires.attachChild(ndWires); + - armatureNode = new ArmatureNode(armature, joints, wires, outlines); + armatureNode = new ArmatureNode(armature, joints, wires, outlines, deformingJoints); this.attachChild(armatureNode); + displayNonDeformingJoint(false); //interJointWires = new ArmatureInterJointsWire(armature, bonesLength, guessJointsOrientation); //wires = new Geometry(name + "_interwires", interJointWires); // this.attachChild(wires); } + public void displayNonDeformingJoint(boolean display) { + joints.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always); + outlines.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always); + wires.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always); + } + protected void initialize(AssetManager assetManager) { Material matWires = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); matWires.setBoolean("VertexColor", true); diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java index 2b717adfd..258eb4834 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java @@ -41,8 +41,7 @@ import com.jme3.scene.*; import com.jme3.scene.shape.Line; import java.nio.FloatBuffer; -import java.util.HashMap; -import java.util.Map; +import java.util.*; /** * The class that displays either wires between the bones' heads if no length @@ -74,17 +73,17 @@ public class ArmatureNode extends Node { * * @param armature the armature that will be shown */ - public ArmatureNode(Armature armature, Node joints, Node wires, Node outlines) { + public ArmatureNode(Armature armature, Node joints, Node wires, Node outlines, List deformingJoints) { this.armature = armature; for (Joint joint : armature.getRoots()) { - createSkeletonGeoms(joint, joints, wires, outlines); + createSkeletonGeoms(joint, joints, wires, outlines, deformingJoints); } this.updateModelBound(); } - protected final void createSkeletonGeoms(Joint joint, Node joints, Node wires, Node outlines) { + protected final void createSkeletonGeoms(Joint joint, Node joints, Node wires, Node outlines, List deformingJoints) { Vector3f start = joint.getModelTransform().getTranslation().clone(); Vector3f end = null; @@ -93,9 +92,11 @@ public class ArmatureNode extends Node { end = joint.getChildren().get(0).getModelTransform().getTranslation().clone(); } + boolean deforms = deformingJoints.contains(joint); + Geometry jGeom = new Geometry(joint.getName() + "Joint", new JointShape()); jGeom.setLocalTranslation(start); - joints.attachChild(jGeom); + attach(joints, deforms, jGeom); Geometry bGeom = null; Geometry bGeomO = null; if (end != null) { @@ -107,14 +108,22 @@ public class ArmatureNode extends Node { bGeom.setUserData("start", wires.getWorldTransform().transformVector(start, start)); bGeom.setUserData("end", wires.getWorldTransform().transformVector(end, end)); bGeom.setQueueBucket(RenderQueue.Bucket.Transparent); - wires.attachChild(bGeom); - outlines.attachChild(bGeomO); + attach(wires, deforms, bGeom); + attach(outlines, deforms, bGeomO); } jointToGeoms.put(joint, new Geometry[]{jGeom, bGeom, bGeomO}); for (Joint child : joint.getChildren()) { - createSkeletonGeoms(child, joints, wires, outlines); + createSkeletonGeoms(child, joints, wires, outlines, deformingJoints); + } + } + + private void attach(Node parent, boolean deforms, Geometry geom) { + if (deforms) { + parent.attachChild(geom); + } else { + ((Node) parent.getChild(0)).attachChild(geom); } } diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index 1525165ad..df4aa4eb8 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -41,9 +41,9 @@ public class TestAnimMigration extends SimpleApplication { rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); - //Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); //Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); - Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); + //Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); //Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml"); AnimMigrationUtils.migrate(model); @@ -52,7 +52,7 @@ public class TestAnimMigration extends SimpleApplication { debugAppState = new ArmatureDebugAppState(); - //stateManager.attach(debugAppState); + stateManager.attach(debugAppState); setupModel(model); @@ -108,6 +108,15 @@ public class TestAnimMigration extends SimpleApplication { } } }, "nextAnim"); + inputManager.addMapping("toggleArmature", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed) { + debugAppState.setEnabled(!debugAppState.isEnabled()); + } + } + }, "toggleArmature"); } private void setupModel(Spatial model) { From 886f8da2f7bde19169aec255883d29b6e4f92bd8 Mon Sep 17 00:00:00 2001 From: Nehon Date: Tue, 26 Dec 2017 01:02:11 +0100 Subject: [PATCH 14/54] Further Armature debugger enhancements --- .../main/java/com/jme3/math/MathUtils.java | 21 ++- .../debug/custom/ArmatureDebugAppState.java | 10 +- .../scene/debug/custom/ArmatureDebugger.java | 62 +++++---- .../debug/custom/ArmatureInterJointsWire.java | 131 +++++++----------- .../jme3/scene/debug/custom/ArmatureNode.java | 130 +++++++++++------ .../Common/MatDefs/Misc/DashedLine.j3md | 59 ++++++++ .../MatDefs/ShaderNodes/Misc/Dashed.j3sn | 37 +++++ .../MatDefs/ShaderNodes/Misc/Dashed100.frag | 10 ++ .../ShaderNodes/Misc/PerspectiveDivide.j3sn | 30 ++++ .../Misc/PerspectiveDivide100.frag | 3 + .../model/anim/TestAnimMigration.java | 10 +- 11 files changed, 341 insertions(+), 162 deletions(-) create mode 100644 jme3-core/src/main/resources/Common/MatDefs/Misc/DashedLine.j3md create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed.j3sn create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed100.frag create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/PerspectiveDivide.j3sn create mode 100644 jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/PerspectiveDivide100.frag diff --git a/jme3-core/src/main/java/com/jme3/math/MathUtils.java b/jme3-core/src/main/java/com/jme3/math/MathUtils.java index 234d5f61b..8c22700b1 100644 --- a/jme3-core/src/main/java/com/jme3/math/MathUtils.java +++ b/jme3-core/src/main/java/com/jme3/math/MathUtils.java @@ -1,5 +1,6 @@ package com.jme3.math; +import com.jme3.renderer.Camera; import com.jme3.util.TempVars; /** @@ -164,7 +165,19 @@ public class MathUtils { } - public static float raySegmentShortestDistance(Ray ray, Vector3f segStart, Vector3f segEnd) { + /** + * Returns the shortest distance between a Ray and a segment. + * The segment is defined by a start position and an end position in world space + * The distance returned will be in world space (world units). + * If the camera parameter is not null the distance will be returned in screen space (pixels) + * + * @param ray The ray + * @param segStart The start position of the segment in world space + * @param segEnd The end position of the segment in world space + * @param camera The renderer camera if the distance is required in screen space. Null if the distance is required in world space + * @return the shortest distance between the ray and the segment or -1 if no solution is found. + */ + public static float raySegmentShortestDistance(Ray ray, Vector3f segStart, Vector3f segEnd, Camera camera) { // Algorithm is ported from the C algorithm of // Paul Bourke at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline3d/ TempVars vars = TempVars.get(); @@ -220,6 +233,12 @@ public class MathUtils { return -1; } + if (camera != null) { + //camera is not null let's convert the points in screen space + camera.getScreenCoordinates(resultSegmentPoint1, resultSegmentPoint1); + camera.getScreenCoordinates(resultSegmentPoint2, resultSegmentPoint2); + } + float length = resultSegmentPoint1.subtractLocal(resultSegmentPoint2).length(); vars.release(); return length; diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java index 10bef21ec..edcb287e6 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java @@ -37,7 +37,7 @@ public class ArmatureDebugAppState extends BaseAppState { vp.setClearDepth(true); this.app = app; for (ArmatureDebugger armatureDebugger : armatures.values()) { - armatureDebugger.initialize(app.getAssetManager()); + armatureDebugger.initialize(app.getAssetManager(), app.getCamera()); } app.getInputManager().addListener(actionListener, "shoot", "toggleJoints"); app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); @@ -93,7 +93,7 @@ public class ArmatureDebugAppState extends BaseAppState { armatures.put(armature, ad); debugNode.attachChild(ad); if (isInitialized()) { - ad.initialize(app.getAssetManager()); + ad.initialize(app.getAssetManager(), app.getCamera()); } return ad; } @@ -108,12 +108,6 @@ public class ArmatureDebugAppState extends BaseAppState { } } - /** - * Pick a Target Using the Mouse Pointer.

  1. Map "pick target" action - * to a MouseButtonTrigger.
  2. flyCam.setEnabled(false); - *
  3. inputManager.setCursorVisible(true);
  4. Implement action in - * AnalogListener (TODO).
- */ private ActionListener actionListener = new ActionListener() { public void onAction(String name, boolean isPressed, float tpf) { if (name.equals("shoot") && isPressed) { diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java index aec5f532e..e76f45253 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java @@ -34,16 +34,17 @@ package com.jme3.scene.debug.custom; import com.jme3.anim.Armature; import com.jme3.anim.Joint; -import com.jme3.animation.Bone; import com.jme3.asset.AssetManager; +import com.jme3.collision.Collidable; +import com.jme3.collision.CollisionResults; import com.jme3.material.Material; import com.jme3.material.RenderState; +import com.jme3.renderer.Camera; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.Geometry; import com.jme3.scene.Node; import com.jme3.texture.Texture; -import java.util.ArrayList; import java.util.List; /** @@ -63,15 +64,11 @@ public class ArmatureDebugger extends Node { private Node joints; private Node outlines; private Node wires; - private List deformingJoints; - /** * The dotted lines between a bone's tail and the had of its children. Not * available if the length data was not provided. */ private ArmatureInterJointsWire interJointWires; - //private Geometry wires; - private List selectedJoints = new ArrayList(); public ArmatureDebugger() { } @@ -86,7 +83,6 @@ public class ArmatureDebugger extends Node { */ public ArmatureDebugger(String name, Armature armature, List deformingJoints) { super(name); - this.deformingJoints = deformingJoints; this.armature = armature; armature.update(); @@ -102,25 +98,40 @@ public class ArmatureDebugger extends Node { joints.attachChild(ndJoints); outlines.attachChild(ndOutlines); wires.attachChild(ndWires); - + Node outlineDashed = new Node("Outlines Dashed"); + Node wiresDashed = new Node("Wires Dashed"); + wiresDashed.attachChild(new Node("dashed non defrom")); + outlineDashed.attachChild(new Node("dashed non defrom")); + outlines.attachChild(outlineDashed); + wires.attachChild(wiresDashed); armatureNode = new ArmatureNode(armature, joints, wires, outlines, deformingJoints); this.attachChild(armatureNode); displayNonDeformingJoint(false); - //interJointWires = new ArmatureInterJointsWire(armature, bonesLength, guessJointsOrientation); - //wires = new Geometry(name + "_interwires", interJointWires); - // this.attachChild(wires); } public void displayNonDeformingJoint(boolean display) { joints.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always); outlines.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always); wires.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always); + ((Node) outlines.getChild(1)).getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always); + ((Node) wires.getChild(1)).getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always); } - protected void initialize(AssetManager assetManager) { + protected void initialize(AssetManager assetManager, Camera camera) { + + armatureNode.setCamera(camera); + + Material matJoints = new Material(assetManager, "Common/MatDefs/Misc/Billboard.j3md"); + Texture t = assetManager.loadTexture("Common/Textures/dot.png"); + matJoints.setTexture("Texture", t); + matJoints.getAdditionalRenderState().setDepthTest(false); + matJoints.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + joints.setQueueBucket(RenderQueue.Bucket.Translucent); + joints.setMaterial(matJoints); + Material matWires = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); matWires.setBoolean("VertexColor", true); matWires.getAdditionalRenderState().setLineWidth(3); @@ -128,19 +139,16 @@ public class ArmatureDebugger extends Node { Material matOutline = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); matOutline.setBoolean("VertexColor", true); - //matOutline.setColor("Color", ColorRGBA.White); matOutline.getAdditionalRenderState().setLineWidth(5); outlines.setMaterial(matOutline); - Material matJoints = new Material(assetManager, "Common/MatDefs/Misc/Billboard.j3md"); - Texture t = assetManager.loadTexture("Common/Textures/dot.png"); -// matJoints.setBoolean("VertexColor", true); -// matJoints.setTexture("ColorMap", t); - matJoints.setTexture("Texture", t); - matJoints.getAdditionalRenderState().setDepthTest(false); - matJoints.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - joints.setQueueBucket(RenderQueue.Bucket.Translucent); - joints.setMaterial(matJoints); + Material matOutline2 = new Material(assetManager, "Common/MatDefs/Misc/DashedLine.j3md"); + matOutline2.getAdditionalRenderState().setLineWidth(1); + outlines.getChild(1).setMaterial(matOutline2); + + Material matWires2 = new Material(assetManager, "Common/MatDefs/Misc/DashedLine.j3md"); + matWires2.getAdditionalRenderState().setLineWidth(1); + wires.getChild(1).setMaterial(matWires2); } @@ -152,9 +160,13 @@ public class ArmatureDebugger extends Node { public void updateLogicalState(float tpf) { super.updateLogicalState(tpf); armatureNode.updateGeometry(); - if (interJointWires != null) { - // interJointWires.updateGeometry(); - } + } + + @Override + public int collideWith(Collidable other, CollisionResults results) { + + return armatureNode.collideWith(other, results); + } protected Joint select(Geometry g) { diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java index 3c93e7dad..01c24cb49 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java @@ -33,16 +33,12 @@ package com.jme3.scene.debug.custom; */ -import com.jme3.anim.Armature; -import com.jme3.anim.Joint; import com.jme3.math.Vector3f; import com.jme3.scene.Mesh; import com.jme3.scene.VertexBuffer; -import com.jme3.scene.VertexBuffer.*; -import com.jme3.util.BufferUtils; +import com.jme3.scene.VertexBuffer.Type; import java.nio.FloatBuffer; -import java.util.Map; /** * A class that displays a dotted line between a bone tail and its childrens' heads. @@ -50,90 +46,69 @@ import java.util.Map; * @author Marcin Roguski (Kaelthas) */ public class ArmatureInterJointsWire extends Mesh { - private static final int POINT_AMOUNT = 50; - /** - * The amount of connections between bones. - */ - private int connectionsAmount; - /** - * The armature that will be showed. - */ - private Armature armature; - /** - * The map between the bone index and its length. - */ - private Map boneLengths; + private Vector3f tmp = new Vector3f(); - private boolean guessBonesOrientation = false; - /** - * Creates buffers for points. Each line has POINT_AMOUNT of points. - * - * @param armature the armature that will be showed - * @param boneLengths the lengths of the bones - */ - public ArmatureInterJointsWire(Armature armature, Map boneLengths, boolean guessBonesOrientation) { - this.armature = armature; + public ArmatureInterJointsWire(Vector3f start, Vector3f[] ends) { + setMode(Mode.Lines); + updateGeometry(start, ends); + } - for (Joint joint : armature.getRoots()) { - this.countConnections(joint); + protected void updateGeometry(Vector3f start, Vector3f[] ends) { + float[] pos = new float[ends.length * 3 + 3]; + pos[0] = start.x; + pos[1] = start.y; + pos[2] = start.z; + int index; + for (int i = 0; i < ends.length; i++) { + index = i * 3 + 3; + pos[index] = ends[i].x; + pos[index + 1] = ends[i].y; + pos[index + 2] = ends[i].z; } + setBuffer(Type.Position, 3, pos); - this.setMode(Mode.Points); - this.boneLengths = boneLengths; - - VertexBuffer pb = new VertexBuffer(Type.Position); - FloatBuffer fpb = BufferUtils.createFloatBuffer(POINT_AMOUNT * connectionsAmount * 3); - pb.setupData(Usage.Stream, 3, Format.Float, fpb); - this.setBuffer(pb); + float[] texCoord = new float[ends.length * 2 + 2]; + texCoord[0] = 0; + texCoord[1] = 0; + for (int i = 0; i < ends.length * 2; i++) { + texCoord[i + 2] = tmp.set(start).subtractLocal(ends[i / 2]).length(); + } + setBuffer(Type.TexCoord, 2, texCoord); - this.guessBonesOrientation = guessBonesOrientation; - this.updateCounts(); + float[] normal = new float[ends.length * 3 + 3]; + for (int i = 0; i < ends.length * 3 + 3; i += 3) { + normal[i] = start.x; + normal[i + 1] = start.y; + normal[i + 2] = start.z; + } + setBuffer(Type.Normal, 3, normal); + + short[] id = new short[ends.length * 2]; + index = 1; + for (int i = 0; i < ends.length * 2; i += 2) { + id[i] = 0; + id[i + 1] = (short) (index); + index++; + } + setBuffer(Type.Index, 2, id); + updateBound(); } /** - * The method updates the geometry according to the positions of the bones. + * Update the start and end points of the line. */ - public void updateGeometry() { - VertexBuffer vb = this.getBuffer(Type.Position); - FloatBuffer posBuf = this.getFloatBuffer(Type.Position); - posBuf.clear(); - for (int i = 0; i < armature.getJointCount(); ++i) { - Joint joint = armature.getJoint(i); - Vector3f parentTail = joint.getModelTransform().getTranslation().add(joint.getModelTransform().getRotation().mult(Vector3f.UNIT_Y.mult(boneLengths.get(i)))); - - if (guessBonesOrientation) { - parentTail = joint.getModelTransform().getTranslation(); - } - - for (Joint child : joint.getChildren()) { - Vector3f childHead = child.getModelTransform().getTranslation(); - Vector3f v = childHead.subtract(parentTail); - float len = v.length(); - float pointDelta = 1f / POINT_AMOUNT; - v.normalizeLocal().multLocal(pointDelta); - Vector3f pointPosition = parentTail.clone(); - for (int j = 0; j < POINT_AMOUNT * len; ++j) { - posBuf.put(pointPosition.getX()).put(pointPosition.getY()).put(pointPosition.getZ()); - pointPosition.addLocal(v); - } - } - } - posBuf.flip(); - vb.updateData(posBuf); + public void updatePoints(Vector3f start, Vector3f end) { + VertexBuffer posBuf = getBuffer(Type.Position); - this.updateBound(); - } + FloatBuffer fb = (FloatBuffer) posBuf.getData(); + fb.rewind(); + fb.put(start.x).put(start.y).put(start.z); + fb.put(end.x).put(end.y).put(end.z); - /** - * Th method counts the connections between bones. - * - * @param joint the bone where counting starts - */ - private void countConnections(Joint joint) { - for (Joint child : joint.getChildren()) { - ++connectionsAmount; - this.countConnections(child); - } + posBuf.updateData(fb); + + updateBound(); } + } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java index 258eb4834..52bafa771 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java @@ -36,6 +36,7 @@ import com.jme3.anim.Armature; import com.jme3.anim.Joint; import com.jme3.collision.*; import com.jme3.math.*; +import com.jme3.renderer.Camera; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.*; import com.jme3.scene.shape.Line; @@ -60,11 +61,13 @@ public class ArmatureNode extends Node { private Map geomToJoint = new HashMap<>(); private Joint selectedJoint = null; private Vector3f tmpStart = new Vector3f(); - private Vector3f tmpEnd = new Vector3f(); - ColorRGBA selectedColor = ColorRGBA.Orange; - ColorRGBA selectedColorJ = ColorRGBA.Yellow; - ;//new ColorRGBA(0.2f, 1f, 1.0f, 1.0f); - ColorRGBA baseColor = new ColorRGBA(0.05f, 0.05f, 0.05f, 1f); + private List tmpEnds = new ArrayList<>(); + private final static ColorRGBA selectedColor = ColorRGBA.Orange; + private final static ColorRGBA selectedColorJ = ColorRGBA.Yellow; + private final static ColorRGBA outlineColor = ColorRGBA.LightGray; + private final static ColorRGBA baseColor = new ColorRGBA(0.05f, 0.05f, 0.05f, 1f); + + private Camera camera; /** @@ -85,11 +88,14 @@ public class ArmatureNode extends Node { protected final void createSkeletonGeoms(Joint joint, Node joints, Node wires, Node outlines, List deformingJoints) { Vector3f start = joint.getModelTransform().getTranslation().clone(); - Vector3f end = null; - //One child only, the bone direction is from the parent joint to the child joint. - if (joint.getChildren().size() == 1) { - end = joint.getChildren().get(0).getModelTransform().getTranslation().clone(); + Vector3f[] ends = null; + if (!joint.getChildren().isEmpty()) { + ends = new Vector3f[joint.getChildren().size()]; + } + + for (int i = 0; i < joint.getChildren().size(); i++) { + ends[i] = joint.getChildren().get(i).getModelTransform().getTranslation().clone(); } boolean deforms = deformingJoints.contains(joint); @@ -99,19 +105,36 @@ public class ArmatureNode extends Node { attach(joints, deforms, jGeom); Geometry bGeom = null; Geometry bGeomO = null; - if (end != null) { - bGeom = new Geometry(joint.getName() + "Bone", new Line(start, end)); - setColor(bGeom, baseColor); + if (ends != null) { + Mesh m = null; + Mesh mO = null; + Node wireAttach = wires; + Node outlinesAttach = outlines; + if (ends.length == 1) { + m = new Line(start, ends[0]); + mO = new Line(start, ends[0]); + } else { + m = new ArmatureInterJointsWire(start, ends); + mO = new ArmatureInterJointsWire(start, ends); + wireAttach = (Node) wires.getChild(1); + outlinesAttach = null; + } + bGeom = new Geometry(joint.getName() + "Bone", m); + setColor(bGeom, outlinesAttach == null ? outlineColor : baseColor); geomToJoint.put(bGeom, joint); - bGeomO = new Geometry(joint.getName() + "BoneOutline", new Line(start, end)); - setColor(bGeomO, ColorRGBA.White); - bGeom.setUserData("start", wires.getWorldTransform().transformVector(start, start)); - bGeom.setUserData("end", wires.getWorldTransform().transformVector(end, end)); + bGeom.setUserData("start", getWorldTransform().transformVector(start, start)); + for (int i = 0; i < ends.length; i++) { + getWorldTransform().transformVector(ends[i], ends[i]); + } + bGeom.setUserData("end", ends); bGeom.setQueueBucket(RenderQueue.Bucket.Transparent); - attach(wires, deforms, bGeom); - attach(outlines, deforms, bGeomO); + attach(wireAttach, deforms, bGeom); + if (outlinesAttach != null) { + bGeomO = new Geometry(joint.getName() + "BoneOutline", mO); + setColor(bGeomO, outlineColor); + attach(outlinesAttach, deforms, bGeomO); + } } - jointToGeoms.put(joint, new Geometry[]{jGeom, bGeom, bGeomO}); for (Joint child : joint.getChildren()) { @@ -119,6 +142,10 @@ public class ArmatureNode extends Node { } } + public void setCamera(Camera camera) { + this.camera = camera; + } + private void attach(Node parent, boolean deforms, Geometry geom) { if (deforms) { parent.attachChild(geom); @@ -142,7 +169,10 @@ public class ArmatureNode extends Node { Geometry[] geomArray = jointToGeoms.get(selectedJoint); setColor(geomArray[0], selectedColorJ); setColor(geomArray[1], selectedColor); - setColor(geomArray[2], baseColor); + + if (geomArray[2] != null) { + setColor(geomArray[2], baseColor); + } return j; } return null; @@ -154,8 +184,10 @@ public class ArmatureNode extends Node { } Geometry[] geoms = jointToGeoms.get(selectedJoint); setColor(geoms[0], ColorRGBA.White); - setColor(geoms[1], baseColor); - setColor(geoms[2], ColorRGBA.White); + setColor(geoms[1], geoms[2] == null ? outlineColor : baseColor); + if (geoms[2] != null) { + setColor(geoms[2], outlineColor); + } selectedJoint = null; } @@ -171,20 +203,25 @@ public class ArmatureNode extends Node { jGeom.setLocalTranslation(joint.getModelTransform().getTranslation()); Geometry bGeom = geoms[1]; if (bGeom != null) { - tmpStart.set(joint.getModelTransform().getTranslation()); - boolean hasEnd = false; - if (joint.getChildren().size() == 1) { - tmpEnd.set(joint.getChildren().get(0).getModelTransform().getTranslation()); - hasEnd = true; - } - if (hasEnd) { - updateBoneMesh(bGeom); + Vector3f start = bGeom.getUserData("start"); + Vector3f[] ends = bGeom.getUserData("end"); + start.set(joint.getModelTransform().getTranslation()); + if (ends != null) { + tmpEnds.clear(); + for (int i = 0; i < joint.getChildren().size(); i++) { + ends[i].set(joint.getChildren().get(i).getModelTransform().getTranslation()); + } + updateBoneMesh(bGeom, start, ends); Geometry bGeomO = geoms[2]; - updateBoneMesh(bGeomO); - Vector3f start = bGeom.getUserData("start"); - Vector3f end = bGeom.getUserData("end"); - bGeom.setUserData("start", bGeom.getParent().getWorldTransform().transformVector(tmpStart, start)); - bGeom.setUserData("end", bGeom.getParent().getWorldTransform().transformVector(tmpEnd, end)); + if (bGeomO != null) { + updateBoneMesh(bGeomO, start, ends); + } + bGeom.setUserData("start", getWorldTransform().transformVector(start, start)); + for (int i = 0; i < ends.length; i++) { + getWorldTransform().transformVector(ends[i], ends[i]); + } + bGeom.setUserData("end", ends); + } } } @@ -201,25 +238,28 @@ public class ArmatureNode extends Node { } int nbCol = 0; for (Geometry g : geomToJoint.keySet()) { - float len = MathUtils.raySegmentShortestDistance((Ray) other, (Vector3f) g.getUserData("start"), (Vector3f) g.getUserData("end")); - if (len > 0 && len < 0.1f) { - CollisionResult res = new CollisionResult(); - res.setGeometry(g); - results.addCollision(res); - nbCol++; + Vector3f start = g.getUserData("start"); + Vector3f[] ends = g.getUserData("end"); + for (int i = 0; i < ends.length; i++) { + float len = MathUtils.raySegmentShortestDistance((Ray) other, start, ends[i], camera); + if (len > 0 && len < 10f) { + CollisionResult res = new CollisionResult(); + res.setGeometry(g); + results.addCollision(res); + nbCol++; + } } } return nbCol; } - private void updateBoneMesh(Geometry bGeom) { + private void updateBoneMesh(Geometry bGeom, Vector3f start, Vector3f[] ends) { VertexBuffer pos = bGeom.getMesh().getBuffer(VertexBuffer.Type.Position); FloatBuffer fb = (FloatBuffer) pos.getData(); fb.rewind(); - fb.put(new float[]{tmpStart.x, tmpStart.y, tmpStart.z, - tmpEnd.x, tmpEnd.y, tmpEnd.z,}); + fb.put(new float[]{start.x, start.y, start.z, + ends[0].x, ends[0].y, ends[0].z,}); pos.updateData(fb); - bGeom.updateModelBound(); } diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/DashedLine.j3md b/jme3-core/src/main/resources/Common/MatDefs/Misc/DashedLine.j3md new file mode 100644 index 000000000..097b44c71 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/DashedLine.j3md @@ -0,0 +1,59 @@ +MaterialDef DashedLine { + MaterialParameters { + } + + Technique { + WorldParameters { + WorldViewProjectionMatrix + Resolution + } + + VertexShaderNodes { + ShaderNode TransformPosition { + Definition: TransformPosition: Common/MatDefs/ShaderNodes/Basic/TransformPosition.j3sn + InputMappings { + transformsMatrix = WorldParam.WorldViewProjectionMatrix + inputPosition = Attr.inNormal + } + OutputMappings { + } + } + ShaderNode PerspectiveDivide { + Definition: PerspectiveDivide: Common/MatDefs/ShaderNodes/Misc/PerspectiveDivide.j3sn + InputMappings { + inVec = TransformPosition.outPosition + } + OutputMappings { + } + } + ShaderNode CommonVert { + Definition: CommonVert: Common/MatDefs/ShaderNodes/Common/CommonVert.j3sn + InputMappings { + worldViewProjectionMatrix = WorldParam.WorldViewProjectionMatrix + modelPosition = Attr.inPosition + vertColor = Attr.inColor + texCoord1 = Attr.inTexCoord + } + OutputMappings { + Global.position = projPosition + } + } + } + + FragmentShaderNodes { + ShaderNode Dashed { + Definition: Dashed: Common/MatDefs/ShaderNodes/Misc/Dashed.j3sn + InputMappings { + texCoord = CommonVert.texCoord1 + inColor = CommonVert.vertColor + resolution = WorldParam.Resolution + startPos = PerspectiveDivide.outVec + } + OutputMappings { + Global.color = outColor + } + } + } + + } +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed.j3sn b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed.j3sn new file mode 100644 index 000000000..87f235121 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed.j3sn @@ -0,0 +1,37 @@ +ShaderNodeDefinitions{ + ShaderNodeDefinition Dashed { + //Vertex/Fragment + Type: Fragment + + //Shader GLSL: + Shader GLSL100: Common/MatDefs/ShaderNodes/Misc/Dashed100.frag + + Documentation{ + //type documentation here. This is optional but recommended + Output a dashed line (better have a LINE mode mesh) + + //@input + @input vec2 texCoord the texture coordinates + @input vec4 inColor The input color + + + //@output + @output vec4 outColor The modified output color + } + Input { + //all the node inputs + // + vec2 texCoord + vec4 inColor + vec4 startPos + vec2 resolution + + + } + Output { + //all the node outputs + // + vec4 outColor + } + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed100.frag b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed100.frag new file mode 100644 index 000000000..522971863 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed100.frag @@ -0,0 +1,10 @@ +void main(){ + startPos.xy = (startPos * 0.5 + 0.5).xy * resolution; + float len = distance(gl_FragCoord.xy,startPos.xy); + outColor = inColor; + float factor = int(len * 0.25); + if(mod(factor, 2) > 0.0){ + discard; + } + +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/PerspectiveDivide.j3sn b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/PerspectiveDivide.j3sn new file mode 100644 index 000000000..61ada6c2c --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/PerspectiveDivide.j3sn @@ -0,0 +1,30 @@ +ShaderNodeDefinitions{ + ShaderNodeDefinition PerspectiveDivide { + //Vertex/Fragment + Type: Vertex + + //Shader GLSL: + Shader GLSL100: Common/MatDefs/ShaderNodes/Misc/PerspectiveDivide100.frag + + Documentation{ + //type documentation here. This is optional but recommended + performs inVec.xyz / inVec.w + + //@input + @input vec4 inVec The input vector + + //@output + @output vec4 outVec The modified output vector + } + Input { + //all the node inputs + // + vec4 inVec + } + Output { + //all the node outputs + // + vec4 outVec + } + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/PerspectiveDivide100.frag b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/PerspectiveDivide100.frag new file mode 100644 index 000000000..0e4f1d5be --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/PerspectiveDivide100.frag @@ -0,0 +1,3 @@ +void main(){ + outVec = vec4(inVec.xyz / inVec.w,1.0); +} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index df4aa4eb8..836238e2b 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -26,7 +26,7 @@ public class TestAnimMigration extends SimpleApplication { ArmatureDebugAppState debugAppState; AnimComposer composer; Queue anims = new LinkedList<>(); - boolean playAnim = true; + boolean playAnim = false; public static void main(String... argv) { TestAnimMigration app = new TestAnimMigration(); @@ -36,15 +36,15 @@ public class TestAnimMigration extends SimpleApplication { @Override public void simpleInitApp() { setTimer(new EraseTimer()); - //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); + cam.setFrustumPerspective(45f, (float) cam.getWidth() / cam.getHeight(), 0.1f, 100f); viewPort.setBackgroundColor(ColorRGBA.DarkGray); rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); - Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); - //Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + //Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + //Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml").scale(0.2f).move(0, 1, 0); //Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); - //Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml"); + Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml").scale(0.02f); AnimMigrationUtils.migrate(model); From a32c34939a3db118740de362141da97651a4135f Mon Sep 17 00:00:00 2001 From: Nehon Date: Tue, 26 Dec 2017 09:48:34 +0100 Subject: [PATCH 15/54] Display origin of the Armature debugger --- .../debug/custom/ArmatureInterJointsWire.java | 20 ++++++++++++++----- .../jme3/scene/debug/custom/ArmatureNode.java | 19 ++++++++++-------- .../java/jme3test/model/TestGltfLoading.java | 4 ++-- .../model/anim/TestAnimMigration.java | 8 ++++---- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java index 01c24cb49..b1c62a55b 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java @@ -98,17 +98,27 @@ public class ArmatureInterJointsWire extends Mesh { /** * Update the start and end points of the line. */ - public void updatePoints(Vector3f start, Vector3f end) { + public void updatePoints(Vector3f start, Vector3f[] ends) { VertexBuffer posBuf = getBuffer(Type.Position); - FloatBuffer fb = (FloatBuffer) posBuf.getData(); fb.rewind(); fb.put(start.x).put(start.y).put(start.z); - fb.put(end.x).put(end.y).put(end.z); - + for (int i = 0; i < ends.length; i++) { + fb.put(ends[i].x); + fb.put(ends[i].y); + fb.put(ends[i].z); + } posBuf.updateData(fb); - updateBound(); + VertexBuffer normBuf = getBuffer(Type.Normal); + fb = (FloatBuffer) normBuf.getData(); + fb.rewind(); + for (int i = 0; i < ends.length * 3 + 3; i += 3) { + fb.put(start.x); + fb.put(start.y); + fb.put(start.z); + } + normBuf.updateData(fb); } } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java index 52bafa771..121366b1a 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java @@ -79,6 +79,10 @@ public class ArmatureNode extends Node { public ArmatureNode(Armature armature, Node joints, Node wires, Node outlines, List deformingJoints) { this.armature = armature; + Geometry origin = new Geometry("Armature Origin", new JointShape()); + setColor(origin, ColorRGBA.Green); + attach(joints, true, origin); + for (Joint joint : armature.getRoots()) { createSkeletonGeoms(joint, joints, wires, outlines, deformingJoints); } @@ -253,14 +257,13 @@ public class ArmatureNode extends Node { return nbCol; } - private void updateBoneMesh(Geometry bGeom, Vector3f start, Vector3f[] ends) { - VertexBuffer pos = bGeom.getMesh().getBuffer(VertexBuffer.Type.Position); - FloatBuffer fb = (FloatBuffer) pos.getData(); - fb.rewind(); - fb.put(new float[]{start.x, start.y, start.z, - ends[0].x, ends[0].y, ends[0].z,}); - pos.updateData(fb); - bGeom.updateModelBound(); + private void updateBoneMesh(Geometry geom, Vector3f start, Vector3f[] ends) { + if (geom.getMesh() instanceof ArmatureInterJointsWire) { + ((ArmatureInterJointsWire) geom.getMesh()).updatePoints(start, ends); + } else if (geom.getMesh() instanceof Line) { + ((Line) geom.getMesh()).updatePoints(start, ends[0]); + } + geom.updateModelBound(); } private void setColor(Geometry g, ColorRGBA color) { diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index 66e06e13e..ad85f3efe 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -116,12 +116,12 @@ public class TestGltfLoading extends SimpleApplication { //loadModel("Models/gltf/elephant/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/buffalo/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/war/scene.gltf", new Vector3f(0, -1, 0), 0.1f); - //loadModel("Models/gltf/ganjaarl/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + loadModel("Models/gltf/ganjaarl/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/hero/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/mercy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/crab/scene.gltf", Vector3f.ZERO, 1); //loadModel("Models/gltf/manta/scene.gltf", Vector3f.ZERO, 0.2f); -// loadModel("Models/gltf/bone/scene.gltf", Vector3f.ZERO, 0.1f); + //loadModel("Models/gltf/bone/scene.gltf", Vector3f.ZERO, 0.1f); // loadModel("Models/gltf/box/box.gltf", Vector3f.ZERO, 1); // loadModel("Models/gltf/duck/Duck.gltf", new Vector3f(0, -1, 0), 1); // loadModel("Models/gltf/damagedHelmet/damagedHelmet.gltf", Vector3f.ZERO, 1); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index 836238e2b..5bff5fdd5 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -26,7 +26,7 @@ public class TestAnimMigration extends SimpleApplication { ArmatureDebugAppState debugAppState; AnimComposer composer; Queue anims = new LinkedList<>(); - boolean playAnim = false; + boolean playAnim = true; public static void main(String... argv) { TestAnimMigration app = new TestAnimMigration(); @@ -41,10 +41,10 @@ public class TestAnimMigration extends SimpleApplication { rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); - //Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); - //Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml").scale(0.2f).move(0, 1, 0); +// Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml").scale(0.2f).move(0, 1, 0); //Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); - Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml").scale(0.02f); + // Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml").scale(0.02f); AnimMigrationUtils.migrate(model); From 02a1fd544a1e9868413ac1978e8e995f5607227a Mon Sep 17 00:00:00 2001 From: Nehon Date: Tue, 26 Dec 2017 10:30:54 +0100 Subject: [PATCH 16/54] Better armature joint selection --- .../debug/custom/ArmatureDebugAppState.java | 28 ++++++++++--- .../scene/debug/custom/ArmatureDebugger.java | 5 +++ .../jme3/scene/debug/custom/ArmatureNode.java | 39 +++++++++++++++---- .../model/anim/TestAnimMigration.java | 4 +- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java index edcb287e6..fb16135e6 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java @@ -23,11 +23,13 @@ import java.util.*; */ public class ArmatureDebugAppState extends BaseAppState { + public static final float CLICK_MAX_DELAY = 0.2f; private Node debugNode = new Node("debugNode"); private Map armatures = new HashMap<>(); private Map selectedBones = new HashMap<>(); private Application app; private boolean displayAllJoints = false; + private float clickDelay = -1; ViewPort vp; @Override @@ -66,8 +68,12 @@ public class ArmatureDebugAppState extends BaseAppState { @Override public void update(float tpf) { + if (clickDelay > -1) { + clickDelay += tpf; + } debugNode.updateLogicalState(tpf); debugNode.updateGeometricState(); + } public ArmatureDebugger addArmatureFrom(SkinningControl skinningControl) { @@ -111,19 +117,31 @@ public class ArmatureDebugAppState extends BaseAppState { private ActionListener actionListener = new ActionListener() { public void onAction(String name, boolean isPressed, float tpf) { if (name.equals("shoot") && isPressed) { - CollisionResults results = new CollisionResults(); + clickDelay = 0; + } + if (name.equals("shoot") && !isPressed && clickDelay < CLICK_MAX_DELAY) { Vector2f click2d = app.getInputManager().getCursorPosition(); - Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); - Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); - Ray ray = new Ray(click3d, dir); + CollisionResults results = new CollisionResults(); + //first check 2d collision with joints + for (ArmatureDebugger ad : armatures.values()) { + ad.pick(click2d, results); + } + + if (results.size() == 0) { + //no result, let's ray cast for bone geometries + Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); + Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); + Ray ray = new Ray(click3d, dir); + debugNode.collideWith(ray, results); + } - debugNode.collideWith(ray, results); if (results.size() == 0) { for (ArmatureDebugger ad : armatures.values()) { ad.select(null); } return; } + // The closest result is the target that the player picked: Geometry target = results.getClosestCollision().getGeometry(); for (ArmatureDebugger ad : armatures.values()) { diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java index e76f45253..ac93bfaaf 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java @@ -39,6 +39,7 @@ import com.jme3.collision.Collidable; import com.jme3.collision.CollisionResults; import com.jme3.material.Material; import com.jme3.material.RenderState; +import com.jme3.math.Vector2f; import com.jme3.renderer.Camera; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.Geometry; @@ -152,6 +153,10 @@ public class ArmatureDebugger extends Node { } + public int pick(Vector2f cursor, CollisionResults results) { + return armatureNode.pick(cursor, results); + } + public Armature getArmature() { return armature; } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java index 121366b1a..7f8b26c2d 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java @@ -50,6 +50,7 @@ import java.util.*; */ public class ArmatureNode extends Node { + public static final float PIXEL_BOX = 10f; /** * The armature to be displayed. */ @@ -60,8 +61,7 @@ public class ArmatureNode extends Node { private Map jointToGeoms = new HashMap<>(); private Map geomToJoint = new HashMap<>(); private Joint selectedJoint = null; - private Vector3f tmpStart = new Vector3f(); - private List tmpEnds = new ArrayList<>(); + private Vector3f tmp = new Vector3f(); private final static ColorRGBA selectedColor = ColorRGBA.Orange; private final static ColorRGBA selectedColorJ = ColorRGBA.Yellow; private final static ColorRGBA outlineColor = ColorRGBA.LightGray; @@ -109,7 +109,9 @@ public class ArmatureNode extends Node { attach(joints, deforms, jGeom); Geometry bGeom = null; Geometry bGeomO = null; - if (ends != null) { + if (ends == null) { + geomToJoint.put(jGeom, joint); + } else { Mesh m = null; Mesh mO = null; Node wireAttach = wires; @@ -172,7 +174,10 @@ public class ArmatureNode extends Node { selectedJoint = j; Geometry[] geomArray = jointToGeoms.get(selectedJoint); setColor(geomArray[0], selectedColorJ); - setColor(geomArray[1], selectedColor); + + if (geomArray[1] != null) { + setColor(geomArray[1], selectedColor); + } if (geomArray[2] != null) { setColor(geomArray[2], baseColor); @@ -188,7 +193,9 @@ public class ArmatureNode extends Node { } Geometry[] geoms = jointToGeoms.get(selectedJoint); setColor(geoms[0], ColorRGBA.White); - setColor(geoms[1], geoms[2] == null ? outlineColor : baseColor); + if (geoms[1] != null) { + setColor(geoms[1], geoms[2] == null ? outlineColor : baseColor); + } if (geoms[2] != null) { setColor(geoms[2], outlineColor); } @@ -211,7 +218,6 @@ public class ArmatureNode extends Node { Vector3f[] ends = bGeom.getUserData("end"); start.set(joint.getModelTransform().getTranslation()); if (ends != null) { - tmpEnds.clear(); for (int i = 0; i < joint.getChildren().size(); i++) { ends[i].set(joint.getChildren().get(i).getModelTransform().getTranslation()); } @@ -235,6 +241,22 @@ public class ArmatureNode extends Node { } } + public int pick(Vector2f cursor, CollisionResults results) { + + for (Geometry g : geomToJoint.keySet()) { + if (g.getMesh() instanceof JointShape) { + camera.getScreenCoordinates(g.getWorldTranslation(), tmp); + if (cursor.x <= tmp.x + PIXEL_BOX && cursor.x >= tmp.x - PIXEL_BOX + && cursor.y <= tmp.y + PIXEL_BOX && cursor.y >= tmp.y - PIXEL_BOX) { + CollisionResult res = new CollisionResult(); + res.setGeometry(g); + results.addCollision(res); + } + } + } + return 0; + } + @Override public int collideWith(Collidable other, CollisionResults results) { if (!(other instanceof Ray)) { @@ -242,11 +264,14 @@ public class ArmatureNode extends Node { } int nbCol = 0; for (Geometry g : geomToJoint.keySet()) { + if (g.getMesh() instanceof JointShape) { + continue; + } Vector3f start = g.getUserData("start"); Vector3f[] ends = g.getUserData("end"); for (int i = 0; i < ends.length; i++) { float len = MathUtils.raySegmentShortestDistance((Ray) other, start, ends[i], camera); - if (len > 0 && len < 10f) { + if (len > 0 && len < PIXEL_BOX) { CollisionResult res = new CollisionResult(); res.setGeometry(g); results.addCollision(res); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index 5bff5fdd5..3b0e8e0a3 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -41,8 +41,8 @@ public class TestAnimMigration extends SimpleApplication { rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); -// Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); - Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml").scale(0.2f).move(0, 1, 0); + Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + // Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml").scale(0.2f).move(0, 1, 0); //Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); // Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml").scale(0.02f); From d6b05553b510f349ef6da27ecff90b05791f950c Mon Sep 17 00:00:00 2001 From: Nehon Date: Tue, 26 Dec 2017 23:19:34 +0100 Subject: [PATCH 17/54] Ogre loader now loads animations and armature with the new system --- .../control/KinematicRagdollControl.java | 28 ++-- .../jme3/anim/util/AnimMigrationUtils.java | 24 +-- .../jme3/cinematic/events/AnimationEvent.java | 17 +- .../jme3test/animation/TestCinematic.java | 20 +-- .../java/jme3test/bullet/TestBoneRagdoll.java | 16 +- .../jme3test/bullet/TestRagdollCharacter.java | 12 +- .../java/jme3test/bullet/TestWalkingChar.java | 16 +- .../java/jme3test/export/TestOgreConvert.java | 15 +- .../jme3test/helloworld/HelloAnimation.java | 7 +- .../jme3test/model/anim/TestAnimBlendBug.java | 132 ---------------- .../model/anim/TestAnimMigration.java | 9 +- .../jme3test/model/anim/TestHWSkinning.java | 27 ++-- .../model/anim/TestModelExportingCloning.java | 23 ++- .../jme3test/model/anim/TestOgreAnim.java | 15 +- .../model/anim/TestOgreComplexAnim.java | 23 ++- .../anim/TestSkeletonControlRefresh.java | 15 +- .../jme3test/stress/TestLodGeneration.java | 17 +- .../com/jme3/scene/plugins/ogre/AnimData.java | 13 +- .../jme3/scene/plugins/ogre/MeshLoader.java | 60 +++---- .../scene/plugins/ogre/SkeletonLoader.java | 149 +++++++++--------- .../main/resources/Models/Oto/OtoOldAnim.j3o | Bin 0 -> 389275 bytes .../resources/Models/Sinbad/SinbadOldAnim.j3o | Bin 0 -> 842731 bytes 22 files changed, 212 insertions(+), 426 deletions(-) delete mode 100644 jme3-examples/src/main/java/jme3test/model/anim/TestAnimBlendBug.java create mode 100644 jme3-testdata/src/main/resources/Models/Oto/OtoOldAnim.j3o create mode 100644 jme3-testdata/src/main/resources/Models/Sinbad/SinbadOldAnim.j3o diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/control/KinematicRagdollControl.java b/jme3-bullet/src/common/java/com/jme3/bullet/control/KinematicRagdollControl.java index c1a79c946..49c0e0f34 100644 --- a/jme3-bullet/src/common/java/com/jme3/bullet/control/KinematicRagdollControl.java +++ b/jme3-bullet/src/common/java/com/jme3/bullet/control/KinematicRagdollControl.java @@ -31,36 +31,23 @@ */ package com.jme3.bullet.control; -import com.jme3.animation.AnimControl; -import com.jme3.animation.Bone; -import com.jme3.animation.Skeleton; -import com.jme3.animation.SkeletonControl; +import com.jme3.animation.*; import com.jme3.bullet.PhysicsSpace; -import com.jme3.bullet.collision.PhysicsCollisionEvent; -import com.jme3.bullet.collision.PhysicsCollisionListener; -import com.jme3.bullet.collision.PhysicsCollisionObject; -import com.jme3.bullet.collision.RagdollCollisionListener; +import com.jme3.bullet.collision.*; import com.jme3.bullet.collision.shapes.BoxCollisionShape; import com.jme3.bullet.collision.shapes.HullCollisionShape; -import com.jme3.bullet.control.ragdoll.HumanoidRagdollPreset; -import com.jme3.bullet.control.ragdoll.RagdollPreset; -import com.jme3.bullet.control.ragdoll.RagdollUtils; +import com.jme3.bullet.control.ragdoll.*; import com.jme3.bullet.joints.SixDofJoint; import com.jme3.bullet.objects.PhysicsRigidBody; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; -import com.jme3.export.Savable; -import com.jme3.math.FastMath; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; +import com.jme3.export.*; +import com.jme3.math.*; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.util.TempVars; import com.jme3.util.clone.JmeCloneable; + import java.io.IOException; import java.util.*; import java.util.logging.Level; @@ -91,7 +78,10 @@ import java.util.logging.Logger; *

* * @author Normen Hansen and Rémy Bouquet (Nehon) + * + * TODO this needs to be redone with the new animation system */ +@Deprecated public class KinematicRagdollControl extends AbstractPhysicsControl implements PhysicsCollisionListener, JmeCloneable { protected static final Logger logger = Logger.getLogger(KinematicRagdollControl.class.getName()); diff --git a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java index ba049c3ae..799c4a7d6 100644 --- a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java +++ b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java @@ -80,16 +80,7 @@ public class AnimMigrationUtils { } for (int i = 0; i < staticJoints.length; i++) { - Joint j = staticJoints[i]; - if (j != null) { - // joint has no track , we create one with the default pose - float[] times = new float[]{0}; - Vector3f[] translations = new Vector3f[]{j.getLocalTranslation()}; - Quaternion[] rotations = new Quaternion[]{j.getLocalRotation()}; - Vector3f[] scales = new Vector3f[]{j.getLocalScale()}; - JointTrack track = new JointTrack(j, times, translations, rotations, scales); - clip.addTrack(track); - } + padJointTracks(clip, staticJoints[i]); } composer.addAnimClip(clip); @@ -104,6 +95,19 @@ public class AnimMigrationUtils { } } + public static void padJointTracks(AnimClip clip, Joint staticJoint) { + Joint j = staticJoint; + if (j != null) { + // joint has no track , we create one with the default pose + float[] times = new float[]{0}; + Vector3f[] translations = new Vector3f[]{j.getLocalTranslation()}; + Quaternion[] rotations = new Quaternion[]{j.getLocalRotation()}; + Vector3f[] scales = new Vector3f[]{j.getLocalScale()}; + JointTrack track = new JointTrack(j, times, translations, rotations, scales); + clip.addTrack(track); + } + } + private static class SkeletonControlVisitor implements SceneGraphVisitor { Map skeletonArmatureMap; diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java index 6dfc2a260..9616ebca5 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java @@ -31,24 +31,16 @@ */ package com.jme3.cinematic.events; -import com.jme3.animation.AnimChannel; -import com.jme3.animation.AnimControl; -import com.jme3.animation.LoopMode; +import com.jme3.animation.*; import com.jme3.app.Application; import com.jme3.cinematic.Cinematic; import com.jme3.cinematic.PlayState; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; +import com.jme3.export.*; import com.jme3.scene.Node; import com.jme3.scene.Spatial; -import com.jme3.util.clone.Cloner; -import com.jme3.util.clone.JmeCloneable; + import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import java.util.logging.Logger; /** @@ -60,6 +52,7 @@ import java.util.logging.Logger; * * @author Nehon */ +@Deprecated public class AnimationEvent extends AbstractCinematicEvent { // Version #2: directly keeping track on the model instead of trying to retrieve diff --git a/jme3-examples/src/main/java/jme3test/animation/TestCinematic.java b/jme3-examples/src/main/java/jme3test/animation/TestCinematic.java index 93ad16f7f..8332ffafe 100644 --- a/jme3-examples/src/main/java/jme3test/animation/TestCinematic.java +++ b/jme3-examples/src/main/java/jme3test/animation/TestCinematic.java @@ -31,13 +31,9 @@ */ package jme3test.animation; -import com.jme3.animation.AnimControl; -import com.jme3.animation.AnimationFactory; -import com.jme3.animation.LoopMode; +import com.jme3.animation.*; import com.jme3.app.SimpleApplication; -import com.jme3.cinematic.Cinematic; -import com.jme3.cinematic.MotionPath; -import com.jme3.cinematic.PlayState; +import com.jme3.cinematic.*; import com.jme3.cinematic.events.*; import com.jme3.font.BitmapText; import com.jme3.input.ChaseCamera; @@ -45,21 +41,18 @@ import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.light.DirectionalLight; import com.jme3.material.Material; -import com.jme3.math.ColorRGBA; -import com.jme3.math.FastMath; -import com.jme3.math.Vector3f; +import com.jme3.math.*; import com.jme3.niftygui.NiftyJmeDisplay; import com.jme3.post.FilterPostProcessor; import com.jme3.post.filters.FadeFilter; import com.jme3.renderer.Caps; import com.jme3.renderer.queue.RenderQueue.ShadowMode; -import com.jme3.scene.CameraNode; -import com.jme3.scene.Geometry; -import com.jme3.scene.Spatial; +import com.jme3.scene.*; import com.jme3.scene.shape.Box; import com.jme3.shadow.DirectionalLightShadowRenderer; import de.lessvoid.nifty.Nifty; +//TODO rework this Test when the new animation system is done. public class TestCinematic extends SimpleApplication { private Spatial model; @@ -77,7 +70,6 @@ public class TestCinematic extends SimpleApplication { app.start(); - } @Override @@ -202,7 +194,7 @@ public class TestCinematic extends SimpleApplication { private void createScene() { - model = (Spatial) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + model = assetManager.loadModel("Models/Oto/OtoOldAnim.j3o"); model.center(); model.setShadowMode(ShadowMode.CastAndReceive); rootNode.attachChild(model); diff --git a/jme3-examples/src/main/java/jme3test/bullet/TestBoneRagdoll.java b/jme3-examples/src/main/java/jme3test/bullet/TestBoneRagdoll.java index bc806b018..8b86f1b0c 100644 --- a/jme3-examples/src/main/java/jme3test/bullet/TestBoneRagdoll.java +++ b/jme3-examples/src/main/java/jme3test/bullet/TestBoneRagdoll.java @@ -36,24 +36,17 @@ import com.jme3.app.SimpleApplication; import com.jme3.asset.TextureKey; import com.jme3.bullet.BulletAppState; import com.jme3.bullet.PhysicsSpace; -import com.jme3.bullet.collision.PhysicsCollisionEvent; -import com.jme3.bullet.collision.PhysicsCollisionObject; -import com.jme3.bullet.collision.RagdollCollisionListener; +import com.jme3.bullet.collision.*; import com.jme3.bullet.collision.shapes.SphereCollisionShape; import com.jme3.bullet.control.KinematicRagdollControl; import com.jme3.bullet.control.RigidBodyControl; import com.jme3.font.BitmapText; import com.jme3.input.KeyInput; import com.jme3.input.MouseInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.KeyTrigger; -import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.input.controls.*; import com.jme3.light.DirectionalLight; import com.jme3.material.Material; -import com.jme3.math.ColorRGBA; -import com.jme3.math.FastMath; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; +import com.jme3.math.*; import com.jme3.scene.Geometry; import com.jme3.scene.Node; import com.jme3.scene.debug.SkeletonDebugger; @@ -65,6 +58,7 @@ import com.jme3.texture.Texture; * PHYSICS RAGDOLLS ARE NOT WORKING PROPERLY YET! * @author normenhansen */ +//TODO rework this Test when the new animation system is done. public class TestBoneRagdoll extends SimpleApplication implements RagdollCollisionListener, AnimEventListener { private BulletAppState bulletAppState; @@ -101,7 +95,7 @@ public class TestBoneRagdoll extends SimpleApplication implements RagdollCollisi PhysicsTestHelper.createPhysicsTestWorld(rootNode, assetManager, bulletAppState.getPhysicsSpace()); setupLight(); - model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); + model = (Node) assetManager.loadModel("Models/Sinbad/SinbadOldAnim.j3o"); // model.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X)); diff --git a/jme3-examples/src/main/java/jme3test/bullet/TestRagdollCharacter.java b/jme3-examples/src/main/java/jme3test/bullet/TestRagdollCharacter.java index 5b9e948c8..f94307d6e 100644 --- a/jme3-examples/src/main/java/jme3test/bullet/TestRagdollCharacter.java +++ b/jme3-examples/src/main/java/jme3test/bullet/TestRagdollCharacter.java @@ -31,10 +31,7 @@ */ package jme3test.bullet; -import com.jme3.animation.AnimChannel; -import com.jme3.animation.AnimControl; -import com.jme3.animation.AnimEventListener; -import com.jme3.animation.LoopMode; +import com.jme3.animation.*; import com.jme3.app.SimpleApplication; import com.jme3.asset.TextureKey; import com.jme3.bullet.BulletAppState; @@ -46,9 +43,7 @@ import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.light.DirectionalLight; import com.jme3.material.Material; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector2f; -import com.jme3.math.Vector3f; +import com.jme3.math.*; import com.jme3.renderer.queue.RenderQueue.ShadowMode; import com.jme3.scene.Geometry; import com.jme3.scene.Node; @@ -58,6 +53,7 @@ import com.jme3.texture.Texture; /** * @author normenhansen */ +//TODO rework this Test when the new animation system is done. public class TestRagdollCharacter extends SimpleApplication implements AnimEventListener, ActionListener { BulletAppState bulletAppState; @@ -89,7 +85,7 @@ public class TestRagdollCharacter extends SimpleApplication implements AnimEvent cam.setLocation(new Vector3f(-8,0,-4)); cam.lookAt(new Vector3f(4,0,-7), Vector3f.UNIT_Y); - model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); + model = (Node) assetManager.loadModel("Models/Sinbad/SinbadOldAnim.j3o"); model.lookAt(new Vector3f(0,0,-1), Vector3f.UNIT_Y); model.setLocalTranslation(4, 0, -7f); diff --git a/jme3-examples/src/main/java/jme3test/bullet/TestWalkingChar.java b/jme3-examples/src/main/java/jme3test/bullet/TestWalkingChar.java index d5b42d01b..4359154d1 100644 --- a/jme3-examples/src/main/java/jme3test/bullet/TestWalkingChar.java +++ b/jme3-examples/src/main/java/jme3test/bullet/TestWalkingChar.java @@ -31,10 +31,7 @@ */ package jme3test.bullet; -import com.jme3.animation.AnimChannel; -import com.jme3.animation.AnimControl; -import com.jme3.animation.AnimEventListener; -import com.jme3.animation.LoopMode; +import com.jme3.animation.*; import com.jme3.app.SimpleApplication; import com.jme3.bullet.BulletAppState; import com.jme3.bullet.PhysicsSpace; @@ -54,16 +51,12 @@ import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.light.DirectionalLight; import com.jme3.material.Material; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector2f; -import com.jme3.math.Vector3f; +import com.jme3.math.*; import com.jme3.post.FilterPostProcessor; import com.jme3.post.filters.BloomFilter; import com.jme3.renderer.Camera; import com.jme3.renderer.queue.RenderQueue.ShadowMode; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.Spatial; +import com.jme3.scene.*; import com.jme3.scene.shape.Box; import com.jme3.scene.shape.Sphere; import com.jme3.scene.shape.Sphere.TextureMode; @@ -74,6 +67,7 @@ import com.jme3.terrain.heightmap.ImageBasedHeightMap; import com.jme3.texture.Texture; import com.jme3.texture.Texture.WrapMode; import com.jme3.util.SkyFactory; + import java.util.ArrayList; import java.util.List; @@ -297,7 +291,7 @@ public class TestWalkingChar extends SimpleApplication implements ActionListener private void createCharacter() { CapsuleCollisionShape capsule = new CapsuleCollisionShape(3f, 4f); character = new CharacterControl(capsule, 0.01f); - model = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + model = (Node) assetManager.loadModel("Models/Oto/OtoOldAnim.j3o"); //model.setLocalScale(0.5f); model.addControl(character); character.setPhysicsLocation(new Vector3f(-140, 40, -10)); diff --git a/jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java b/jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java index bfc03c0c5..6495c1de1 100644 --- a/jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java +++ b/jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java @@ -32,8 +32,7 @@ package jme3test.export; -import com.jme3.animation.AnimChannel; -import com.jme3.animation.AnimControl; +import com.jme3.anim.AnimComposer; import com.jme3.app.SimpleApplication; import com.jme3.export.binary.BinaryExporter; import com.jme3.export.binary.BinaryImporter; @@ -42,9 +41,8 @@ import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.scene.Node; import com.jme3.scene.Spatial; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; + +import java.io.*; public class TestOgreConvert extends SimpleApplication { @@ -71,10 +69,9 @@ public class TestOgreConvert extends SimpleApplication { BinaryImporter imp = new BinaryImporter(); imp.setAssetManager(assetManager); Node ogreModelReloaded = (Node) imp.load(bais, null, null); - - AnimControl control = ogreModelReloaded.getControl(AnimControl.class); - AnimChannel chan = control.createChannel(); - chan.setAnim("Walk"); + + AnimComposer composer = ogreModelReloaded.getControl(AnimComposer.class); + composer.setCurrentAnimClip("Walk"); rootNode.attachChild(ogreModelReloaded); } catch (IOException ex){ diff --git a/jme3-examples/src/main/java/jme3test/helloworld/HelloAnimation.java b/jme3-examples/src/main/java/jme3test/helloworld/HelloAnimation.java index bb1587b3e..db9c700c8 100644 --- a/jme3-examples/src/main/java/jme3test/helloworld/HelloAnimation.java +++ b/jme3-examples/src/main/java/jme3test/helloworld/HelloAnimation.java @@ -32,10 +32,7 @@ package jme3test.helloworld; -import com.jme3.animation.AnimChannel; -import com.jme3.animation.AnimControl; -import com.jme3.animation.AnimEventListener; -import com.jme3.animation.LoopMode; +import com.jme3.animation.*; import com.jme3.app.SimpleApplication; import com.jme3.input.KeyInput; import com.jme3.input.controls.ActionListener; @@ -70,7 +67,7 @@ public class HelloAnimation extends SimpleApplication rootNode.addLight(dl); /** Load a model that contains animation */ - player = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + player = (Node) assetManager.loadModel("Models/Oto/OtoOldAnim.j3o"); player.setLocalScale(0.5f); rootNode.attachChild(player); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimBlendBug.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimBlendBug.java deleted file mode 100644 index b57fc83cb..000000000 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimBlendBug.java +++ /dev/null @@ -1,132 +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 jme3test.model.anim; - -import com.jme3.animation.AnimChannel; -import com.jme3.animation.AnimControl; -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.light.DirectionalLight; -import com.jme3.material.Material; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector3f; -import com.jme3.scene.Node; -import com.jme3.scene.debug.SkeletonDebugger; - -public class TestAnimBlendBug extends SimpleApplication implements ActionListener { - -// private AnimControl control; - private AnimChannel channel1, channel2; - private String[] animNames; - - private float blendTime = 0.5f; - private float lockAfterBlending = blendTime + 0.25f; - private float blendingAnimationLock; - - public static void main(String[] args) { - TestAnimBlendBug app = new TestAnimBlendBug(); - app.start(); - } - - public void onAction(String name, boolean value, float tpf) { - if (name.equals("One") && value){ - channel1.setAnim(animNames[4], blendTime); - channel2.setAnim(animNames[4], 0); - channel1.setSpeed(0.25f); - channel2.setSpeed(0.25f); - blendingAnimationLock = lockAfterBlending; - } - } - - public void onPreUpdate(float tpf) { - } - - public void onPostUpdate(float tpf) { - } - - @Override - public void simpleUpdate(float tpf) { - // Is there currently a blending underway? - if (blendingAnimationLock > 0f) { - blendingAnimationLock -= tpf; - } - } - - @Override - public void simpleInitApp() { - inputManager.addMapping("One", new KeyTrigger(KeyInput.KEY_1)); - inputManager.addListener(this, "One"); - - flyCam.setMoveSpeed(100f); - cam.setLocation( new Vector3f( 0f, 150f, -325f ) ); - cam.lookAt( new Vector3f( 0f, 100f, 0f ), Vector3f.UNIT_Y ); - - DirectionalLight dl = new DirectionalLight(); - dl.setDirection(new Vector3f(-0.1f, -0.7f, 1).normalizeLocal()); - dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f)); - rootNode.addLight(dl); - - Node model1 = (Node) assetManager.loadModel("Models/Ninja/Ninja.mesh.xml"); - Node model2 = (Node) assetManager.loadModel("Models/Ninja/Ninja.mesh.xml"); -// Node model2 = model1.clone(); - - model1.setLocalTranslation(-60, 0, 0); - model2.setLocalTranslation(60, 0, 0); - - AnimControl control1 = model1.getControl(AnimControl.class); - animNames = control1.getAnimationNames().toArray(new String[0]); - channel1 = control1.createChannel(); - - AnimControl control2 = model2.getControl(AnimControl.class); - channel2 = control2.createChannel(); - - SkeletonDebugger skeletonDebug = new SkeletonDebugger("skeleton1", control1.getSkeleton()); - Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.getAdditionalRenderState().setWireframe(true); - mat.setColor("Color", ColorRGBA.Red); - mat.setFloat("PointSize", 7f); - mat.getAdditionalRenderState().setDepthTest(false); - skeletonDebug.setMaterial(mat); - model1.attachChild(skeletonDebug); - - skeletonDebug = new SkeletonDebugger("skeleton2", control2.getSkeleton()); - skeletonDebug.setMaterial(mat); - model2.attachChild(skeletonDebug); - - rootNode.attachChild(model1); - rootNode.attachChild(model2); - } - -} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index 3b0e8e0a3..4ed76312a 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -2,7 +2,6 @@ package jme3test.model.anim; import com.jme3.anim.AnimComposer; import com.jme3.anim.SkinningControl; -import com.jme3.anim.util.AnimMigrationUtils; import com.jme3.app.ChaseCameraAppState; import com.jme3.app.SimpleApplication; import com.jme3.input.KeyInput; @@ -41,12 +40,12 @@ public class TestAnimMigration extends SimpleApplication { rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); - Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); - // Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml").scale(0.2f).move(0, 1, 0); + //Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml").scale(0.2f).move(0, 1, 0); //Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); - // Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml").scale(0.02f); + //Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml").scale(0.02f); - AnimMigrationUtils.migrate(model); + // AnimMigrationUtils.migrate(model); rootNode.attachChild(model); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java index 9a2e78804..980d8ca59 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java @@ -31,28 +31,28 @@ */ package jme3test.model.anim; -import com.jme3.animation.*; +import com.jme3.anim.AnimComposer; +import com.jme3.anim.SkinningControl; import com.jme3.app.SimpleApplication; import com.jme3.font.BitmapText; import com.jme3.input.KeyInput; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.light.DirectionalLight; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; +import com.jme3.math.*; import com.jme3.scene.Spatial; + import java.util.ArrayList; import java.util.List; public class TestHWSkinning extends SimpleApplication implements ActionListener{ - private AnimChannel channel; - private AnimControl control; + + private AnimComposer composer; private String[] animNames = {"Dodge", "Walk", "pull", "push"}; private final static int SIZE = 10; private boolean hwSkinningEnable = true; - private List skControls = new ArrayList(); + private List skControls = new ArrayList(); private BitmapText hwsText; public static void main(String[] args) { @@ -77,13 +77,12 @@ public class TestHWSkinning extends SimpleApplication implements ActionListener{ Spatial model = (Spatial) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); model.setLocalScale(0.1f); model.setLocalTranslation(i - SIZE / 2, 0, j - SIZE / 2); - control = model.getControl(AnimControl.class); + composer = model.getControl(AnimComposer.class); - channel = control.createChannel(); - channel.setAnim(animNames[(i + j) % 4]); - SkeletonControl skeletonControl = model.getControl(SkeletonControl.class); - skeletonControl.setHardwareSkinningPreferred(hwSkinningEnable); - skControls.add(skeletonControl); + composer.setCurrentAnimClip(animNames[(i + j) % 4]); + SkinningControl skinningControl = model.getControl(SkinningControl.class); + skinningControl.setHardwareSkinningPreferred(hwSkinningEnable); + skControls.add(skinningControl); rootNode.attachChild(model); } } @@ -96,7 +95,7 @@ public class TestHWSkinning extends SimpleApplication implements ActionListener{ public void onAction(String name, boolean isPressed, float tpf) { if(isPressed && name.equals("toggleHWS")){ hwSkinningEnable = !hwSkinningEnable; - for (SkeletonControl control : skControls) { + for (SkinningControl control : skControls) { control.setHardwareSkinningPreferred(hwSkinningEnable); hwsText.setText("HWS : "+ hwSkinningEnable); } diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestModelExportingCloning.java b/jme3-examples/src/main/java/jme3test/model/anim/TestModelExportingCloning.java index de162907d..80b6feb4c 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestModelExportingCloning.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestModelExportingCloning.java @@ -31,8 +31,7 @@ */ package jme3test.model.anim; -import com.jme3.animation.AnimChannel; -import com.jme3.animation.AnimControl; +import com.jme3.anim.AnimComposer; import com.jme3.app.SimpleApplication; import com.jme3.export.binary.BinaryExporter; import com.jme3.light.DirectionalLight; @@ -57,27 +56,23 @@ public class TestModelExportingCloning extends SimpleApplication { dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f)); rootNode.addLight(dl); - AnimControl control; - AnimChannel channel; - + AnimComposer composer; + Spatial originalModel = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); - control = originalModel.getControl(AnimControl.class); - channel = control.createChannel(); - channel.setAnim("Walk"); + composer = originalModel.getControl(AnimComposer.class); + composer.setCurrentAnimClip("Walk"); rootNode.attachChild(originalModel); Spatial clonedModel = originalModel.clone(); clonedModel.move(10, 0, 0); - control = clonedModel.getControl(AnimControl.class); - channel = control.createChannel(); - channel.setAnim("push"); + composer = clonedModel.getControl(AnimComposer.class); + composer.setCurrentAnimClip("push"); rootNode.attachChild(clonedModel); Spatial exportedModel = BinaryExporter.saveAndLoad(assetManager, originalModel); exportedModel.move(20, 0, 0); - control = exportedModel.getControl(AnimControl.class); - channel = control.createChannel(); - channel.setAnim("pull"); + composer = exportedModel.getControl(AnimComposer.class); + composer.setCurrentAnimClip("pull"); rootNode.attachChild(exportedModel); } } diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestOgreAnim.java b/jme3-examples/src/main/java/jme3test/model/anim/TestOgreAnim.java index b6bd4cd63..a7616f7b5 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestOgreAnim.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestOgreAnim.java @@ -38,15 +38,12 @@ import com.jme3.input.KeyInput; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.light.DirectionalLight; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.Spatial; +import com.jme3.math.*; +import com.jme3.scene.*; import com.jme3.scene.shape.Box; -public class TestOgreAnim extends SimpleApplication +//TODO rework this Test when the new animation system is done. +public class TestOgreAnim extends SimpleApplication implements AnimEventListener, ActionListener { private AnimChannel channel; @@ -69,7 +66,7 @@ public class TestOgreAnim extends SimpleApplication dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f)); rootNode.addLight(dl); - Spatial model = (Spatial) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + Spatial model = (Spatial) assetManager.loadModel("Models/Oto/OtoOldAnim.j3o"); model.center(); control = model.getControl(AnimControl.class); @@ -102,7 +99,7 @@ public class TestOgreAnim extends SimpleApplication // geom.getMesh().createCollisionData(); } - + public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { if (animName.equals("Dodge")){ diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestOgreComplexAnim.java b/jme3-examples/src/main/java/jme3test/model/anim/TestOgreComplexAnim.java index fa1283540..e83fa7a6b 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestOgreComplexAnim.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestOgreComplexAnim.java @@ -32,20 +32,15 @@ package jme3test.model.anim; -import com.jme3.animation.AnimChannel; -import com.jme3.animation.AnimControl; -import com.jme3.animation.Bone; -import com.jme3.animation.LoopMode; +import com.jme3.animation.*; import com.jme3.app.SimpleApplication; import com.jme3.light.DirectionalLight; import com.jme3.material.Material; -import com.jme3.math.ColorRGBA; -import com.jme3.math.FastMath; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; +import com.jme3.math.*; import com.jme3.scene.Node; import com.jme3.scene.debug.SkeletonDebugger; +//TODO rework this Test when the new animation system is done. public class TestOgreComplexAnim extends SimpleApplication { private AnimControl control; @@ -69,7 +64,7 @@ public class TestOgreComplexAnim extends SimpleApplication { dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f)); rootNode.addLight(dl); - Node model = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + Node model = (Node) assetManager.loadModel("Models/Oto/OtoOldAnim.j3o"); control = model.getControl(AnimControl.class); @@ -118,8 +113,8 @@ public class TestOgreComplexAnim extends SimpleApplication { public void simpleUpdate(float tpf){ Bone b = control.getSkeleton().getBone("spinehigh"); Bone b2 = control.getSkeleton().getBone("uparm.left"); - - angle += tpf * rate; + + angle += tpf * rate; if (angle > FastMath.HALF_PI / 2f){ angle = FastMath.HALF_PI / 2f; rate = -1; @@ -133,11 +128,11 @@ public class TestOgreComplexAnim extends SimpleApplication { b.setUserControl(true); b.setUserTransforms(Vector3f.ZERO, q, Vector3f.UNIT_XYZ); - + b2.setUserControl(true); b2.setUserTransforms(Vector3f.ZERO, Quaternion.IDENTITY, new Vector3f(1+angle,1+ angle, 1+angle)); - - + + } } diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestSkeletonControlRefresh.java b/jme3-examples/src/main/java/jme3test/model/anim/TestSkeletonControlRefresh.java index d594f197f..bbc727af3 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestSkeletonControlRefresh.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestSkeletonControlRefresh.java @@ -37,7 +37,6 @@ package jme3test.model.anim; */ - import com.jme3.animation.*; import com.jme3.app.SimpleApplication; import com.jme3.asset.TextureKey; @@ -47,11 +46,7 @@ import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.light.DirectionalLight; import com.jme3.material.Material; -import com.jme3.math.ColorRGBA; -import com.jme3.math.FastMath; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector2f; -import com.jme3.math.Vector3f; +import com.jme3.math.*; import com.jme3.post.FilterPostProcessor; import com.jme3.post.ssao.SSAOFilter; import com.jme3.renderer.queue.RenderQueue; @@ -59,11 +54,11 @@ import com.jme3.scene.Geometry; import com.jme3.scene.Spatial; import com.jme3.scene.shape.Quad; import com.jme3.shadow.DirectionalLightShadowFilter; -import com.jme3.shadow.DirectionalLightShadowRenderer; + import java.util.ArrayList; import java.util.List; -import jme3test.post.SSAOUI; - + +//TODO rework this Test when the new animation system is done. public class TestSkeletonControlRefresh extends SimpleApplication implements ActionListener{ private AnimChannel channel; @@ -97,7 +92,7 @@ public class TestSkeletonControlRefresh extends SimpleApplication implements Act for (int i = 0; i < SIZE; i++) { for (int j = 0; j < SIZE; j++) { - Spatial model = (Spatial) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + Spatial model = (Spatial) assetManager.loadModel("Models/Oto/OtoOldAnim.j3o"); //setting a different material model.setMaterial(m.clone()); model.setLocalScale(0.1f); diff --git a/jme3-examples/src/main/java/jme3test/stress/TestLodGeneration.java b/jme3-examples/src/main/java/jme3test/stress/TestLodGeneration.java index cf8dbb64b..5400e98d8 100644 --- a/jme3-examples/src/main/java/jme3test/stress/TestLodGeneration.java +++ b/jme3-examples/src/main/java/jme3test/stress/TestLodGeneration.java @@ -31,9 +31,8 @@ */ package jme3test.stress; +import com.jme3.anim.SkinningControl; import com.jme3.animation.AnimChannel; -import com.jme3.animation.AnimControl; -import com.jme3.animation.SkeletonControl; import com.jme3.app.SimpleApplication; import com.jme3.bounding.BoundingBox; import com.jme3.font.BitmapText; @@ -43,18 +42,14 @@ import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.light.AmbientLight; import com.jme3.light.DirectionalLight; -import com.jme3.math.ColorRGBA; -import com.jme3.math.FastMath; -import com.jme3.math.Vector3f; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.Spatial; -import com.jme3.scene.VertexBuffer; +import com.jme3.math.*; +import com.jme3.scene.*; +import jme3tools.optimize.LodGenerator; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ScheduledThreadPoolExecutor; -import jme3tools.optimize.LodGenerator; public class TestLodGeneration extends SimpleApplication { @@ -101,7 +96,7 @@ public class TestLodGeneration extends SimpleApplication { // ch = model.getControl(AnimControl.class).createChannel(); // ch.setAnim("Wave"); - SkeletonControl c = model.getControl(SkeletonControl.class); + SkinningControl c = model.getControl(SkinningControl.class); if (c != null) { c.setEnabled(false); } diff --git a/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/AnimData.java b/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/AnimData.java index 1b1eb2a9b..281926a46 100644 --- a/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/AnimData.java +++ b/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/AnimData.java @@ -31,17 +31,18 @@ */ package com.jme3.scene.plugins.ogre; -import com.jme3.animation.Animation; -import com.jme3.animation.Skeleton; +import com.jme3.anim.AnimClip; +import com.jme3.anim.Armature; + import java.util.ArrayList; public class AnimData { - public final Skeleton skeleton; - public final ArrayList anims; + public final Armature armature; + public final ArrayList anims; - public AnimData(Skeleton skeleton, ArrayList anims) { - this.skeleton = skeleton; + public AnimData(Armature armature, ArrayList anims) { + this.armature = armature; this.anims = anims; } } diff --git a/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/MeshLoader.java b/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/MeshLoader.java index baa30a0d1..01474b5c4 100644 --- a/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/MeshLoader.java +++ b/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/MeshLoader.java @@ -31,9 +31,7 @@ */ package com.jme3.scene.plugins.ogre; -import com.jme3.animation.AnimControl; -import com.jme3.animation.Animation; -import com.jme3.animation.SkeletonControl; +import com.jme3.anim.*; import com.jme3.asset.*; import com.jme3.material.Material; import com.jme3.material.MaterialList; @@ -41,30 +39,23 @@ import com.jme3.math.ColorRGBA; import com.jme3.renderer.queue.RenderQueue; import com.jme3.renderer.queue.RenderQueue.Bucket; import com.jme3.scene.*; -import com.jme3.scene.VertexBuffer.Format; -import com.jme3.scene.VertexBuffer.Type; -import com.jme3.scene.VertexBuffer.Usage; +import com.jme3.scene.VertexBuffer.*; import com.jme3.scene.plugins.ogre.matext.OgreMaterialKey; -import com.jme3.util.BufferUtils; -import com.jme3.util.IntMap; +import com.jme3.util.*; import com.jme3.util.IntMap.Entry; -import com.jme3.util.PlaceholderAssets; -import static com.jme3.util.xml.SAXUtil.*; +import org.xml.sax.*; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; import java.io.IOException; import java.io.InputStreamReader; import java.nio.*; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParserFactory; -import org.xml.sax.Attributes; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.DefaultHandler; + +import static com.jme3.util.xml.SAXUtil.*; /** * Loads Ogre3D mesh.xml files. @@ -799,35 +790,28 @@ public class MeshLoader extends DefaultHandler implements AssetLoader { for (int i = 0; i < geoms.size(); i++) { Geometry g = geoms.get(i); Mesh m = geoms.get(i).getMesh(); - - //FIXME the parameter is now useless. - //It was !HADWARE_SKINNING before, but since toggleing - //HW skinning does not happen at load time it was always true. - //We should use something similar as for the HWBoneIndex and - //HWBoneWeight : create the vertex buffers empty so that they - //are put in the cache, and really populate them the first time - //software skinning is used on the mesh. - m.generateBindPose(true); - + m.generateBindPose(); } // Put the animations in the AnimControl - HashMap anims = new HashMap(); - ArrayList animList = animData.anims; + HashMap anims = new HashMap<>(); + ArrayList animList = animData.anims; for (int i = 0; i < animList.size(); i++) { - Animation anim = animList.get(i); + AnimClip anim = animList.get(i); anims.put(anim.getName(), anim); } - AnimControl ctrl = new AnimControl(animData.skeleton); - ctrl.setAnimations(anims); - model.addControl(ctrl); + AnimComposer composer = new AnimComposer(); + for (AnimClip clip : anims.values()) { + composer.addAnimClip(clip); + } + model.addControl(composer); // Put the skeleton in the skeleton control - SkeletonControl skeletonControl = new SkeletonControl(animData.skeleton); + SkinningControl skinningControl = new SkinningControl(animData.armature); // This will acquire the targets from the node - model.addControl(skeletonControl); + model.addControl(skinningControl); } return model; diff --git a/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java b/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java index 56dc95bb6..b1350c93c 100644 --- a/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java +++ b/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java @@ -31,45 +31,35 @@ */ package com.jme3.scene.plugins.ogre; -import com.jme3.animation.Animation; -import com.jme3.animation.Bone; -import com.jme3.animation.BoneTrack; -import com.jme3.animation.Skeleton; +import com.jme3.anim.*; +import com.jme3.anim.util.AnimMigrationUtils; import com.jme3.asset.AssetInfo; import com.jme3.asset.AssetLoader; -import com.jme3.asset.AssetManager; import com.jme3.math.Quaternion; import com.jme3.math.Vector3f; import com.jme3.util.xml.SAXUtil; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.Stack; -import java.util.logging.Logger; +import org.xml.sax.*; +import org.xml.sax.helpers.DefaultHandler; + import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; -import org.xml.sax.Attributes; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.DefaultHandler; +import java.io.*; +import java.util.*; +import java.util.logging.Logger; public class SkeletonLoader extends DefaultHandler implements AssetLoader { private static final Logger logger = Logger.getLogger(SceneLoader.class.getName()); - private AssetManager assetManager; + //private AssetManager assetManager; private Stack elementStack = new Stack(); - private HashMap indexToBone = new HashMap(); - private HashMap nameToBone = new HashMap(); - private BoneTrack track; - private ArrayList tracks = new ArrayList(); - private Animation animation; - private ArrayList animations; - private Bone bone; - private Skeleton skeleton; + private HashMap indexToJoint = new HashMap<>(); + private HashMap nameToJoint = new HashMap<>(); + private JointTrack track; + private ArrayList tracks = new ArrayList<>(); + private AnimClip animClip; + private ArrayList animClips; + private Joint joint; + private Armature armature; private ArrayList times = new ArrayList(); private ArrayList translations = new ArrayList(); private ArrayList rotations = new ArrayList(); @@ -80,6 +70,7 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { private Vector3f scale; private float angle; private Vector3f axis; + private List unusedJoints = new ArrayList<>(); public void startElement(String uri, String localName, String qName, Attributes attribs) throws SAXException { if (qName.equals("position") || qName.equals("translate")) { @@ -99,38 +90,40 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { assert elementStack.peek().equals("track"); } else if (qName.equals("track")) { assert elementStack.peek().equals("tracks"); - String boneName = SAXUtil.parseString(attribs.getValue("bone")); - Bone bone = nameToBone.get(boneName); - int index = skeleton.getBoneIndex(bone); - track = new BoneTrack(index); + String jointName = SAXUtil.parseString(attribs.getValue("bone")); + joint = nameToJoint.get(jointName); + track = new JointTrack(); + track.setTarget(joint); } else if (qName.equals("boneparent")) { assert elementStack.peek().equals("bonehierarchy"); - String boneName = attribs.getValue("bone"); + String jointName = attribs.getValue("bone"); String parentName = attribs.getValue("parent"); - Bone bone = nameToBone.get(boneName); - Bone parent = nameToBone.get(parentName); - parent.addChild(bone); + Joint joint = nameToJoint.get(jointName); + Joint parent = nameToJoint.get(parentName); + parent.addChild(joint); } else if (qName.equals("bone")) { assert elementStack.peek().equals("bones"); // insert bone into indexed map - bone = new Bone(attribs.getValue("name")); + joint = new Joint(attribs.getValue("name")); int id = SAXUtil.parseInt(attribs.getValue("id")); - indexToBone.put(id, bone); - nameToBone.put(bone.getName(), bone); + indexToJoint.put(id, joint); + nameToJoint.put(joint.getName(), joint); } else if (qName.equals("tracks")) { assert elementStack.peek().equals("animation"); tracks.clear(); + unusedJoints.clear(); + unusedJoints.addAll(nameToJoint.values()); } else if (qName.equals("animation")) { assert elementStack.peek().equals("animations"); String name = SAXUtil.parseString(attribs.getValue("name")); - float length = SAXUtil.parseFloat(attribs.getValue("length")); - animation = new Animation(name, length); + //float length = SAXUtil.parseFloat(attribs.getValue("length")); + animClip = new AnimClip(name); } else if (qName.equals("bonehierarchy")) { assert elementStack.peek().equals("skeleton"); } else if (qName.equals("animations")) { assert elementStack.peek().equals("skeleton"); - animations = new ArrayList(); + animClips = new ArrayList<>(); } else if (qName.equals("bones")) { assert elementStack.peek().equals("skeleton"); } else if (qName.equals("skeleton")) { @@ -149,32 +142,42 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { angle = 0; axis = null; } else if (qName.equals("bone")) { - bone.setBindTransforms(position, rotation, scale); - bone = null; + joint.getLocalTransform().setTranslation(position); + joint.getLocalTransform().setRotation(rotation); + if (scale != null) { + joint.getLocalTransform().setScale(scale); + } + joint = null; position = null; rotation = null; scale = null; } else if (qName.equals("bonehierarchy")) { - Bone[] bones = new Bone[indexToBone.size()]; - // find bones without a parent and attach them to the skeleton - // also assign the bones to the bonelist - for (Map.Entry entry : indexToBone.entrySet()) { - Bone bone = entry.getValue(); - bones[entry.getKey()] = bone; + Joint[] joints = new Joint[indexToJoint.size()]; + // find joints without a parent and attach them to the armature + // also assign the joints to the jointList + for (Map.Entry entry : indexToJoint.entrySet()) { + Joint joint = entry.getValue(); + joints[entry.getKey()] = joint; } - indexToBone.clear(); - skeleton = new Skeleton(bones); + indexToJoint.clear(); + armature = new Armature(joints); + armature.setBindPose(); } else if (qName.equals("animation")) { - animations.add(animation); - animation = null; + //nameToJoint contains the joints with no track + for (Joint j : unusedJoints) { + AnimMigrationUtils.padJointTracks(animClip, j); + } + animClips.add(animClip); + animClip = null; } else if (qName.equals("track")) { if (track != null) { // if track has keyframes tracks.add(track); + unusedJoints.remove(joint); track = null; } } else if (qName.equals("tracks")) { - BoneTrack[] trackList = tracks.toArray(new BoneTrack[tracks.size()]); - animation.setTracks(trackList); + JointTrack[] trackList = tracks.toArray(new JointTrack[tracks.size()]); + animClip.setTracks(trackList); tracks.clear(); } else if (qName.equals("keyframe")) { assert time >= 0; @@ -182,14 +185,13 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { assert rotation != null; times.add(time); - translations.add(position); - rotations.add(rotation); + translations.add(position.addLocal(joint.getLocalTranslation())); + rotations.add(joint.getLocalRotation().mult(rotation, rotation)); if (scale != null) { - scales.add(scale); + scales.add(scale.multLocal(joint.getLocalScale())); }else{ scales.add(new Vector3f(1,1,1)); } - time = -1; position = null; rotation = null; @@ -206,7 +208,6 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { Vector3f[] scalesArray = scales.toArray(new Vector3f[scales.size()]); track.setKeyframes(timesArray, transArray, rotArray, scalesArray); - //track.setKeyframes(timesArray, transArray, rotArray); } else { track = null; } @@ -216,7 +217,7 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { rotations.clear(); scales.clear(); } else if (qName.equals("skeleton")) { - nameToBone.clear(); + nameToJoint.clear(); } assert elementStack.peek().equals(qName); elementStack.pop(); @@ -228,17 +229,17 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { */ private void fullReset() { elementStack.clear(); - indexToBone.clear(); - nameToBone.clear(); + indexToJoint.clear(); + nameToJoint.clear(); track = null; tracks.clear(); - animation = null; - if (animations != null) { - animations.clear(); + animClip = null; + if (animClips != null) { + animClips.clear(); } - bone = null; - skeleton = null; + joint = null; + armature = null; times.clear(); rotations.clear(); translations.clear(); @@ -266,12 +267,12 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { xr.setErrorHandler(this); InputStreamReader r = new InputStreamReader(in); xr.parse(new InputSource(r)); - if (animations == null) { - animations = new ArrayList(); + if (animClips == null) { + animClips = new ArrayList(); } - AnimData data = new AnimData(skeleton, animations); - skeleton = null; - animations = null; + AnimData data = new AnimData(armature, animClips); + armature = null; + animClips = null; return data; } catch (SAXException ex) { IOException ioEx = new IOException("Error while parsing Ogre3D dotScene"); @@ -288,7 +289,7 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { } public Object load(AssetInfo info) throws IOException { - assetManager = info.getManager(); + //AssetManager assetManager = info.getManager(); InputStream in = null; try { in = info.openStream(); diff --git a/jme3-testdata/src/main/resources/Models/Oto/OtoOldAnim.j3o b/jme3-testdata/src/main/resources/Models/Oto/OtoOldAnim.j3o new file mode 100644 index 0000000000000000000000000000000000000000..101dd8405eacf4d6193b2a5e72ede4bd608d3f82 GIT binary patch literal 389275 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 ^8oMyUL`nUwO_ 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 HBT@8!)^b&d literal 0 HcmV?d00001 diff --git a/jme3-testdata/src/main/resources/Models/Sinbad/SinbadOldAnim.j3o b/jme3-testdata/src/main/resources/Models/Sinbad/SinbadOldAnim.j3o new file mode 100644 index 0000000000000000000000000000000000000000..4df92c80af1516e0865c5d5d1d5840442e049b82 GIT binary patch literal 842731 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

Jt%AiuVBKX5Wr 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

+ *

For example:

+ *
Tweens.callTweenMethod(1, myObject, "foo", "bar")
+ *

Would work for any of the following method signatures:

+ *
+     *    void foo( float t, String arg )
+     *    void foo( double t, String arg )
+     *    void foo( String arg, float t )
+     *    void foo( String arg, double t )
+     *  
+ */ + public static Tween callTweenMethod(double length, Object target, String method, Object... args) { + return new CallTweenMethod(length, target, method, args); + } + + private static interface CurveFunction { + public double curve(double input); + } + + /** + * Curve function for Hermite interpolation ala GLSL smoothstep(). + */ + private static class SmoothStep implements CurveFunction { + + @Override + public double curve(double t) { + if (t < 0) { + return 0; + } else if (t > 1) { + return 1; + } + return t * t * (3 - 2 * t); + } + } + + private static class Sine implements CurveFunction { + + @Override + public double curve(double t) { + if (t < 0) { + return 0; + } else if (t > 1) { + return 1; + } + // Sine starting at -90 will go from -1 to 1 through 0 + double result = Math.sin(t * Math.PI - Math.PI * 0.5); + return (result + 1) * 0.5; + } + } + + private static class Curve implements Tween { + private final Tween delegate; + private final CurveFunction func; + private final double length; + + public Curve(Tween delegate, CurveFunction func) { + this.delegate = delegate; + this.func = func; + this.length = delegate.getLength(); + } + + @Override + public double getLength() { + return length; + } + + @Override + public boolean interpolate(double t) { + // Sanity check the inputs + if (t < 0) { + return true; + } + + if (length == 0) { + // Caller did something strange but we'll allow it + return delegate.interpolate(t); + } + + t = func.curve(t / length); + return delegate.interpolate(t * length); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[delegate=" + delegate + ", func=" + func + "]"; + } + } + + private static class Sequence implements Tween { + private final Tween[] delegates; + private int current = 0; + private double baseTime; + private double length; + + public Sequence(Tween... delegates) { + this.delegates = delegates; + for (Tween t : delegates) { + length += t.getLength(); + } + } + + @Override + public double getLength() { + return length; + } + + @Override + public boolean interpolate(double t) { + + // Sanity check the inputs + if (t < 0) { + return true; + } + + if (t < baseTime) { + // We've rolled back before the current sequence step + // which means we need to reset and start forward + // again. We have no idea how to 'roll back' and + // this is the only way to maintain consistency. + // The only 'normal' case where this happens is when looping + // in which case a full rollback is appropriate. + current = 0; + baseTime = 0; + } + + if (current >= delegates.length) { + return false; + } + + // Skip any that are done + while (!delegates[current].interpolate(t - baseTime)) { + // Time to go to the next one + baseTime += delegates[current].getLength(); + current++; + if (current >= delegates.length) { + return false; + } + } + + return true; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[delegates=" + Arrays.asList(delegates) + "]"; + } + } + + private static class Parallel implements Tween { + private final Tween[] delegates; + private final boolean[] done; + private double length; + private double lastTime; + + public Parallel(Tween... delegates) { + this.delegates = delegates; + done = new boolean[delegates.length]; + + for (Tween t : delegates) { + if (t.getLength() > length) { + length = t.getLength(); + } + } + } + + @Override + public double getLength() { + return length; + } + + protected void reset() { + for (int i = 0; i < done.length; i++) { + done[i] = false; + } + } + + @Override + public boolean interpolate(double t) { + // Sanity check the inputs + if (t < 0) { + return true; + } + + if (t < lastTime) { + // We've rolled back before the last time we were given. + // This means we may have 'done'ed a few tasks that now + // need to be run again. Better to just reset and start + // over. As mentioned in the Sequence task, the only 'normal' + // use-case for time rolling backwards is when looping. And + // in that case, we want to start from the beginning anyway. + reset(); + } + lastTime = t; + + int runningCount = delegates.length; + for (int i = 0; i < delegates.length; i++) { + if (!done[i]) { + done[i] = !delegates[i].interpolate(t); + } + if (done[i]) { + runningCount--; + } + } + return runningCount > 0; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[delegates=" + Arrays.asList(delegates) + "]"; + } + } + + private static class Delay extends AbstractTween { + + public Delay(double length) { + super(length); + } + + @Override + protected void doInterpolate(double t) { + } + } + + private static class Stretch implements Tween { + + private final Tween delegate; + private final double length; + private final double scale; + + public Stretch(Tween delegate, double length) { + this.delegate = delegate; + this.length = length; + + // Caller desires delegate to be 'length' instead of + // it's actual length so we will calculate a time scale + // If the desired length is longer than delegate's then + // we need to feed time in slower, ie: scale < 1 + if (length != 0) { + this.scale = delegate.getLength() / length; + } else { + this.scale = 0; + } + } + + @Override + public double getLength() { + return length; + } + + @Override + public boolean interpolate(double t) { + if (t < 0) { + return true; + } + if (length > 0) { + t *= scale; + } else { + t = length; + } + return delegate.interpolate(t); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[delegate=" + delegate + ", length=" + length + "]"; + } + } + + private static class CallMethod extends AbstractTween { + + private Object target; + private Method method; + private Object[] args; + + public CallMethod(Object target, String methodName, Object... args) { + super(0); + if (target == null) { + throw new IllegalArgumentException("Target cannot be null."); + } + this.target = target; + this.args = args; + + // Lookup the method + if (args == null) { + this.method = findMethod(target.getClass(), methodName); + } else { + this.method = findMethod(target.getClass(), methodName, args); + } + if (this.method == null) { + throw new IllegalArgumentException("Method not found for:" + methodName + " on type:" + target.getClass()); + } + this.method.setAccessible(true); + } + + private static Method findMethod(Class type, String name, Object... args) { + for (Method m : type.getDeclaredMethods()) { + if (!Objects.equals(m.getName(), name)) { + continue; + } + Class[] paramTypes = m.getParameterTypes(); + if (paramTypes.length != args.length) { + continue; + } + int matches = 0; + for (int i = 0; i < args.length; i++) { + if (paramTypes[i].isInstance(args[i]) + || Primitives.wrap(paramTypes[i]).isInstance(args[i])) { + matches++; + } + } + if (matches == args.length) { + return m; + } + } + if (type.getSuperclass() != null) { + return findMethod(type.getSuperclass(), name, args); + } + return null; + } + + @Override + protected void doInterpolate(double t) { + try { + method.invoke(target, args); + } catch (IllegalAccessException e) { + throw new RuntimeException("Error running method:" + method + " for object:" + target, e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error running method:" + method + " for object:" + target, e); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[method=" + method + ", parms=" + Arrays.asList(args) + "]"; + } + } + + private static class CallTweenMethod extends AbstractTween { + + private Object target; + private Method method; + private Object[] args; + private int tIndex = -1; + private boolean isFloat = false; + + public CallTweenMethod(double length, Object target, String methodName, Object... args) { + super(length); + if (target == null) { + throw new IllegalArgumentException("Target cannot be null."); + } + this.target = target; + + // Lookup the method + this.method = findMethod(target.getClass(), methodName, args); + if (this.method == null) { + throw new IllegalArgumentException("Method not found for:" + methodName + " on type:" + target.getClass()); + } + this.method.setAccessible(true); + + // So now setup the real args list + this.args = new Object[args.length + 1]; + if (tIndex == 0) { + for (int i = 0; i < args.length; i++) { + this.args[i + 1] = args[i]; + } + } else { + for (int i = 0; i < args.length; i++) { + this.args[i] = args[i]; + } + } + } + + private static boolean isFloatType(Class type) { + return type == Float.TYPE || type == Float.class; + } + + private static boolean isDoubleType(Class type) { + return type == Double.TYPE || type == Double.class; + } + + private Method findMethod(Class type, String name, Object... args) { + for (Method m : type.getDeclaredMethods()) { + if (!Objects.equals(m.getName(), name)) { + continue; + } + Class[] paramTypes = m.getParameterTypes(); + if (paramTypes.length != args.length + 1) { + if (log.isLoggable(Level.FINE)) { + log.log(Level.FINE, "Param lengths of [" + m + "] differ. method arg count:" + paramTypes.length + " lookging for:" + (args.length + 1)); + } + continue; + } + + // We accept the 't' parameter as either first or last + // so we'll see which one matches. + if (isFloatType(paramTypes[0]) || isDoubleType(paramTypes[0])) { + // Try it as the first parameter + int matches = 0; + + for (int i = 1; i < paramTypes.length; i++) { + if (paramTypes[i].isInstance(args[i - 1])) { + matches++; + } + } + if (matches == args.length) { + // Then this is our method and this is how we are configured + tIndex = 0; + isFloat = isFloatType(paramTypes[0]); + } else { + if (log.isLoggable(Level.FINE)) { + log.log(Level.FINE, m + " Leading float check failed because of type mismatches, for:" + m); + } + } + } + if (tIndex >= 0) { + return m; + } + + // Else try it at the end + int last = paramTypes.length - 1; + if (isFloatType(paramTypes[last]) || isDoubleType(paramTypes[last])) { + int matches = 0; + + for (int i = 0; i < last; i++) { + if (paramTypes[i].isInstance(args[i])) { + matches++; + } + } + if (matches == args.length) { + // Then this is our method and this is how we are configured + tIndex = last; + isFloat = isFloatType(paramTypes[last]); + return m; + } else { + if (log.isLoggable(Level.FINE)) { + log.log(Level.FINE, "Trailing float check failed because of type mismatches, for:" + m); + } + } + } + } + if (type.getSuperclass() != null) { + return findMethod(type.getSuperclass(), name, args); + } + return null; + } + + @Override + protected void doInterpolate(double t) { + try { + if (isFloat) { + args[tIndex] = (float) t; + } else { + args[tIndex] = t; + } + method.invoke(target, args); + } catch (IllegalAccessException e) { + throw new RuntimeException("Error running method:" + method + " for object:" + target, e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error running method:" + method + " for object:" + target, e); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[method=" + method + ", parms=" + Arrays.asList(args) + "]"; + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java index 9c7037715..ea283f8ec 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java @@ -2,10 +2,7 @@ package com.jme3.anim.tween.action; import com.jme3.anim.tween.Tween; import com.jme3.anim.util.Weighted; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; +import com.jme3.export.*; import java.io.IOException; @@ -49,16 +46,4 @@ public abstract class Action implements Tween, Weighted { public void setParentAction(Action parentAction) { this.parentAction = parentAction; } - - @Override - public void read(JmeImporter im) throws IOException { - InputCapsule ic = im.getCapsule(this); - tweens = (Tween[]) ic.readSavableArray("tweens", null); - } - - @Override - public void write(JmeExporter ex) throws IOException { - OutputCapsule oc = ex.getCapsule(this); - oc.write(tweens, "tweens", null); - } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java new file mode 100644 index 000000000..eb6afe01f --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java @@ -0,0 +1,80 @@ +package com.jme3.anim.tween.action; + +import com.jme3.anim.tween.Tween; +import com.jme3.anim.tween.Tweens; + +public class BlendAction extends Action { + + + private Tween firstActiveTween; + private Tween secondActiveTween; + private BlendSpace blendSpace; + private float blendWeight; + + public BlendAction(BlendSpace blendSpace, Tween... tweens) { + super(tweens); + this.blendSpace = blendSpace; + blendSpace.setBlendAction(this); + + for (Tween tween : tweens) { + if (tween.getLength() > length) { + length = tween.getLength(); + } + } + + //Blending effect maybe unexpected when blended animation don't have the same length + //Stretching any tween that doesn't have the same length. + for (int i = 0; i < tweens.length; i++) { + if (tweens[i].getLength() != length) { + tweens[i] = Tweens.stretch(length, tweens[i]); + } + } + + } + + @Override + public float getWeightForTween(Tween tween) { + blendWeight = blendSpace.getWeight(); + if (tween == firstActiveTween) { + return 1f; + } + return weight * blendWeight; + } + + @Override + public boolean doInterpolate(double t) { + if (firstActiveTween == null) { + blendSpace.getWeight(); + } + + boolean running = this.firstActiveTween.interpolate(t); + this.secondActiveTween.interpolate(t); + + if (!running) { + return false; + } + + return true; + } + + @Override + public void reset() { + + } + + protected Tween[] getTweens() { + return tweens; + } + + public BlendSpace getBlendSpace() { + return blendSpace; + } + + protected void setFirstActiveTween(Tween firstActiveTween) { + this.firstActiveTween = firstActiveTween; + } + + protected void setSecondActiveTween(Tween secondActiveTween) { + this.secondActiveTween = secondActiveTween; + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java new file mode 100644 index 000000000..956e74c3b --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java @@ -0,0 +1,12 @@ +package com.jme3.anim.tween.action; + +import com.jme3.anim.tween.action.BlendAction; + +public interface BlendSpace { + + public void setBlendAction(BlendAction action); + + public float getWeight(); + + public void setValue(float value); +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java new file mode 100644 index 000000000..0c086d1e8 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java @@ -0,0 +1,50 @@ +package com.jme3.anim.tween.action; + +import com.jme3.anim.tween.Tween; + +public class LinearBlendSpace implements BlendSpace { + + private BlendAction action; + private float value; + private float maxValue; + private float step; + + public LinearBlendSpace(float maxValue) { + this.maxValue = maxValue; + + } + + @Override + public void setBlendAction(BlendAction action) { + this.action = action; + Tween[] tweens = action.getTweens(); + step = maxValue / (float) (tweens.length - 1); + } + + @Override + public float getWeight() { + Tween[] tweens = action.getTweens(); + float lowStep = 0, highStep = 0; + int lowIndex = 0, highIndex = 0; + for (int i = 0; i < tweens.length && highStep < value; i++) { + lowStep = highStep; + lowIndex = i; + highStep += step; + } + highIndex = lowIndex + 1; + + action.setFirstActiveTween(tweens[lowIndex]); + action.setSecondActiveTween(tweens[highIndex]); + + if (highStep == lowStep) { + return 0; + } + + return (value - lowStep) / (highStep - lowStep); + } + + @Override + public void setValue(float value) { + this.value = value; + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/util/Primitives.java b/jme3-core/src/main/java/com/jme3/anim/util/Primitives.java new file mode 100644 index 000000000..f61a996f8 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/util/Primitives.java @@ -0,0 +1,56 @@ +package com.jme3.anim.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + + +/** + * This is a guava method used in {@link com.jme3.anim.tween.Tweens} class. + * Maybe we should just add guava as a dependency in the engine... + * //TODO do something about this. + */ +public class Primitives { + + /** + * A map from primitive types to their corresponding wrapper types. + */ + private static final Map, Class> PRIMITIVE_TO_WRAPPER_TYPE; + + static { + Map, Class> primToWrap = new HashMap<>(16); + + primToWrap.put(boolean.class, Boolean.class); + primToWrap.put(byte.class, Byte.class); + primToWrap.put(char.class, Character.class); + primToWrap.put(double.class, Double.class); + primToWrap.put(float.class, Float.class); + primToWrap.put(int.class, Integer.class); + primToWrap.put(long.class, Long.class); + primToWrap.put(short.class, Short.class); + primToWrap.put(void.class, Void.class); + + PRIMITIVE_TO_WRAPPER_TYPE = Collections.unmodifiableMap(primToWrap); + } + + /** + * Returns the corresponding wrapper type of {@code type} if it is a primitive type; otherwise + * returns {@code type} itself. Idempotent. + *

+ *

+     *     wrap(int.class) == Integer.class
+     *     wrap(Integer.class) == Integer.class
+     *     wrap(String.class) == String.class
+     * 
+ */ + public static Class wrap(Class type) { + if (type == null) { + throw new IllegalArgumentException("type is null"); + } + + // cast is safe: long.class and Long.class are both of type Class + @SuppressWarnings("unchecked") + Class wrapped = (Class) PRIMITIVE_TO_WRAPPER_TYPE.get(type); + return (wrapped == null) ? type : wrapped; + } +} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index 6f6f0a4c9..a2c40dd60 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -2,11 +2,14 @@ package jme3test.model.anim; import com.jme3.anim.AnimComposer; import com.jme3.anim.SkinningControl; +import com.jme3.anim.tween.action.BlendAction; +import com.jme3.anim.tween.action.LinearBlendSpace; import com.jme3.anim.util.AnimMigrationUtils; import com.jme3.app.ChaseCameraAppState; import com.jme3.app.SimpleApplication; import com.jme3.input.KeyInput; import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.AnalogListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.light.AmbientLight; import com.jme3.light.DirectionalLight; @@ -27,6 +30,8 @@ public class TestAnimMigration extends SimpleApplication { AnimComposer composer; LinkedList anims = new LinkedList<>(); boolean playAnim = false; + BlendAction action; + float blendValue = 2f; public static void main(String... argv) { TestAnimMigration app = new TestAnimMigration(); @@ -117,6 +122,26 @@ public class TestAnimMigration extends SimpleApplication { } } }, "toggleArmature"); + + inputManager.addMapping("blendUp", new KeyTrigger(KeyInput.KEY_UP)); + inputManager.addMapping("blendDown", new KeyTrigger(KeyInput.KEY_DOWN)); + inputManager.addListener(new AnalogListener() { + + @Override + public void onAnalog(String name, float value, float tpf) { + if (name.equals("blendUp")) { + blendValue += value; + blendValue = FastMath.clamp(blendValue, 0, 4); + action.getBlendSpace().setValue(blendValue); + } + if (name.equals("blendDown")) { + blendValue -= value; + blendValue = FastMath.clamp(blendValue, 0, 4); + action.getBlendSpace().setValue(blendValue); + } + System.err.println(blendValue); + } + }, "blendUp", "blendDown"); } private void setupModel(Spatial model) { @@ -138,6 +163,12 @@ public class TestAnimMigration extends SimpleApplication { composer.tweenFromClip("Run"), composer.tweenFromClip("Jumping")); + action = composer.actionBlended("Blend", new LinearBlendSpace(4), + composer.tweenFromClip("Walk"), + composer.tweenFromClip("Jumping")); + + action.getBlendSpace().setValue(2); + // composer.actionSequence("Sequence", // composer.tweenFromClip("Walk"), // composer.tweenFromClip("Dodge"), @@ -145,6 +176,7 @@ public class TestAnimMigration extends SimpleApplication { anims.addFirst("Sequence"); + anims.addFirst("Blend"); if (anims.isEmpty()) { return; From 79549424f3f2a9adc6742467388b3f74a74dbcc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81my=20Bouquet?= Date: Mon, 1 Jan 2018 18:00:43 +0100 Subject: [PATCH 21/54] Better blending structure --- .../main/java/com/jme3/anim/AnimComposer.java | 40 +++--- .../com/jme3/anim/tween/AnimClipTween.java | 87 ------------ .../com/jme3/anim/tween/ContainsTweens.java | 6 + .../main/java/com/jme3/anim/tween/Tweens.java | 30 +++- .../com/jme3/anim/tween/action/Action.java | 39 ++--- .../jme3/anim/tween/action/BaseAction.java | 40 ++++++ .../jme3/anim/tween/action/BlendAction.java | 133 ++++++++++++------ .../anim/tween/action/BlendableAction.java | 83 +++++++++++ .../jme3/anim/tween/action/ClipAction.java | 64 +++++++++ .../anim/tween/action/LinearBlendSpace.java | 14 +- .../anim/tween/action/SequenceAction.java | 75 ---------- .../model/anim/TestAnimMigration.java | 10 +- 12 files changed, 346 insertions(+), 275 deletions(-) delete mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/ContainsTweens.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java delete mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/action/SequenceAction.java diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index 1bdb41176..1814faf72 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -1,11 +1,8 @@ package com.jme3.anim; -import com.jme3.anim.tween.AnimClipTween; import com.jme3.anim.tween.Tween; -import com.jme3.anim.tween.action.Action; -import com.jme3.anim.tween.action.BlendAction; -import com.jme3.anim.tween.action.BlendSpace; -import com.jme3.anim.tween.action.SequenceAction; +import com.jme3.anim.tween.Tweens; +import com.jme3.anim.tween.action.*; import com.jme3.export.*; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; @@ -62,40 +59,37 @@ public class AnimComposer extends AbstractControl { } public void setCurrentAction(String name) { - Action action = action(name); - if (currentAction != null) { - currentAction.reset(); - } - currentAction = action; + currentAction = action(name); time = 0; } public Action action(String name) { Action action = actions.get(name); if (action == null) { - AnimClipTween tween = tweenFromClip(name); - action = new SequenceAction(tween); + AnimClip clip = animClipMap.get(name); + if (clip == null) { + throw new IllegalArgumentException("Cannot find clip named " + name); + } + action = new ClipAction(clip); actions.put(name, action); } return action; } - public AnimClipTween tweenFromClip(String clipName) { - AnimClip clip = animClipMap.get(clipName); - if (clip == null) { - throw new IllegalArgumentException("Cannot find clip named " + clipName); - } - return new AnimClipTween(clip); - } - public SequenceAction actionSequence(String name, Tween... tweens) { - SequenceAction action = new SequenceAction(tweens); + public BaseAction actionSequence(String name, Tween... tweens) { + BaseAction action = new BaseAction(Tweens.sequence(tweens)); actions.put(name, action); return action; } - public BlendAction actionBlended(String name, BlendSpace blendSpace, Tween... tweens) { - BlendAction action = new BlendAction(blendSpace, tweens); + public BlendAction actionBlended(String name, BlendSpace blendSpace, String... clips) { + BlendableAction[] acts = new BlendableAction[clips.length]; + for (int i = 0; i < acts.length; i++) { + BlendableAction ba = (BlendableAction) action(clips[i]); + acts[i] = ba; + } + BlendAction action = new BlendAction(blendSpace, acts); actions.put(name, action); return action; } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java b/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java deleted file mode 100644 index a36936244..000000000 --- a/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.jme3.anim.tween; - -import com.jme3.anim.AnimClip; -import com.jme3.anim.TransformTrack; -import com.jme3.anim.tween.action.Action; -import com.jme3.anim.util.HasLocalTransform; -import com.jme3.anim.util.Weighted; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; -import com.jme3.math.Transform; -import com.jme3.util.clone.Cloner; -import com.jme3.util.clone.JmeCloneable; - -import java.io.IOException; - -public class AnimClipTween implements Tween, Weighted, JmeCloneable { - - private AnimClip clip; - private Transform transform = new Transform(); - private float weight = 1f; - private Action parentAction; - - public AnimClipTween() { - } - - public AnimClipTween(AnimClip clip) { - this.clip = clip; - } - - @Override - public double getLength() { - return clip.getLength(); - } - - @Override - public boolean interpolate(double t) { - // Sanity check the inputs - if (t < 0) { - return true; - } - if (parentAction != null) { - weight = parentAction.getWeightForTween(this); - } - if (weight == 0) { - //weight is 0 let's not interpolate - return t < clip.getLength(); - } - TransformTrack[] tracks = clip.getTracks(); - for (TransformTrack track : tracks) { - HasLocalTransform target = track.getTarget(); - transform.set(target.getLocalTransform()); - track.getTransformAtTime(t, transform); - - if (weight == 1f) { - target.setLocalTransform(transform); - } else { - Transform tr = target.getLocalTransform(); - tr.interpolateTransforms(tr, transform, weight); - target.setLocalTransform(tr); - } - } - return t < clip.getLength(); - } - - - @Override - public Object jmeClone() { - try { - AnimClipTween clone = (AnimClipTween) super.clone(); - return clone; - } catch (CloneNotSupportedException ex) { - throw new AssertionError(); - } - } - - @Override - public void cloneFields(Cloner cloner, Object original) { - clip = cloner.clone(clip); - } - - @Override - public void setParentAction(Action action) { - this.parentAction = action; - } -} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/ContainsTweens.java b/jme3-core/src/main/java/com/jme3/anim/tween/ContainsTweens.java new file mode 100644 index 000000000..b7fe69bfc --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/ContainsTweens.java @@ -0,0 +1,6 @@ +package com.jme3.anim.tween; + +public interface ContainsTweens { + + public Tween[] getTweens(); +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java b/jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java index b369cadce..54ff3986b 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java @@ -221,7 +221,7 @@ public class Tweens { } } - private static class Sequence implements Tween { + private static class Sequence implements Tween, ContainsTweens { private final Tween[] delegates; private int current = 0; private double baseTime; @@ -279,9 +279,14 @@ public class Tweens { public String toString() { return getClass().getSimpleName() + "[delegates=" + Arrays.asList(delegates) + "]"; } + + @Override + public Tween[] getTweens() { + return delegates; + } } - private static class Parallel implements Tween { + private static class Parallel implements Tween, ContainsTweens { private final Tween[] delegates; private final boolean[] done; private double length; @@ -343,6 +348,11 @@ public class Tweens { public String toString() { return getClass().getSimpleName() + "[delegates=" + Arrays.asList(delegates) + "]"; } + + @Override + public Tween[] getTweens() { + return delegates; + } } private static class Delay extends AbstractTween { @@ -356,14 +366,15 @@ public class Tweens { } } - private static class Stretch implements Tween { + private static class Stretch implements Tween, ContainsTweens { - private final Tween delegate; + private final Tween[] delegate = new Tween[1]; private final double length; private final double scale; public Stretch(Tween delegate, double length) { - this.delegate = delegate; + this.delegate[0] = delegate; + this.length = length; // Caller desires delegate to be 'length' instead of @@ -382,6 +393,11 @@ public class Tweens { return length; } + @Override + public Tween[] getTweens() { + return delegate; + } + @Override public boolean interpolate(double t) { if (t < 0) { @@ -392,12 +408,12 @@ public class Tweens { } else { t = length; } - return delegate.interpolate(t); + return delegate[0].interpolate(t); } @Override public String toString() { - return getClass().getSimpleName() + "[delegate=" + delegate + ", length=" + length + "]"; + return getClass().getSimpleName() + "[delegate=" + delegate[0] + ", length=" + length + "]"; } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java index ea283f8ec..d445abf09 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java @@ -1,23 +1,21 @@ package com.jme3.anim.tween.action; import com.jme3.anim.tween.Tween; -import com.jme3.anim.util.Weighted; -import com.jme3.export.*; -import java.io.IOException; +public abstract class Action implements Tween { -public abstract class Action implements Tween, Weighted { - - protected Tween[] tweens; + protected Action[] actions; protected float weight = 1; protected double length; - protected Action parentAction; protected Action(Tween... tweens) { - this.tweens = tweens; - for (Tween tween : tweens) { - if (tween instanceof Weighted) { - ((Weighted) tween).setParentAction(this); + this.actions = new Action[tweens.length]; + for (int i = 0; i < tweens.length; i++) { + Tween tween = tweens[i]; + if (tween instanceof Action) { + this.actions[i] = (Action) tween; + } else { + this.actions[i] = new BaseAction(tween); } } } @@ -27,23 +25,8 @@ public abstract class Action implements Tween, Weighted { return length; } - @Override - public boolean interpolate(double t) { - if (parentAction != null) { - weight = parentAction.getWeightForTween(this); - } - - return doInterpolate(t); + public void setWeight(float weight) { + this.weight = weight; } - public abstract float getWeightForTween(Tween tween); - - public abstract boolean doInterpolate(double t); - - public abstract void reset(); - - @Override - public void setParentAction(Action parentAction) { - this.parentAction = parentAction; - } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java new file mode 100644 index 000000000..6e59ce091 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java @@ -0,0 +1,40 @@ +package com.jme3.anim.tween.action; + +import com.jme3.anim.tween.ContainsTweens; +import com.jme3.anim.tween.Tween; +import com.jme3.util.SafeArrayList; + +public class BaseAction extends Action { + + private Tween tween; + private SafeArrayList subActions = new SafeArrayList<>(Action.class); + + public BaseAction(Tween tween) { + this.tween = tween; + length = tween.getLength(); + gatherActions(tween); + } + + private void gatherActions(Tween tween) { + if (tween instanceof Action) { + subActions.add((Action) tween); + } else if (tween instanceof ContainsTweens) { + Tween[] tweens = ((ContainsTweens) tween).getTweens(); + for (Tween t : tweens) { + gatherActions(t); + } + } + } + + @Override + public void setWeight(float weight) { + for (Action action : subActions.getArray()) { + action.setWeight(weight); + } + } + + @Override + public boolean interpolate(double t) { + return tween.interpolate(t); + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java index eb6afe01f..97188a11f 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java @@ -1,80 +1,129 @@ package com.jme3.anim.tween.action; -import com.jme3.anim.tween.Tween; -import com.jme3.anim.tween.Tweens; +import com.jme3.anim.util.HasLocalTransform; +import com.jme3.math.Transform; -public class BlendAction extends Action { +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +public class BlendAction extends BlendableAction { - private Tween firstActiveTween; - private Tween secondActiveTween; + private int firstActiveIndex; + private int secondActiveIndex; private BlendSpace blendSpace; private float blendWeight; + private double[] timeFactor; + private Map targetMap = new HashMap<>(); - public BlendAction(BlendSpace blendSpace, Tween... tweens) { - super(tweens); + public BlendAction(BlendSpace blendSpace, BlendableAction... actions) { + super(actions); + timeFactor = new double[actions.length]; this.blendSpace = blendSpace; blendSpace.setBlendAction(this); - for (Tween tween : tweens) { - if (tween.getLength() > length) { - length = tween.getLength(); + for (BlendableAction action : actions) { + if (action.getLength() > length) { + length = action.getLength(); + } + Collection targets = action.getTargets(); + for (HasLocalTransform target : targets) { + Transform t = targetMap.get(target); + if (t == null) { + t = new Transform(); + targetMap.put(target, t); + } } } //Blending effect maybe unexpected when blended animation don't have the same length - //Stretching any tween that doesn't have the same length. - for (int i = 0; i < tweens.length; i++) { - if (tweens[i].getLength() != length) { - tweens[i] = Tweens.stretch(length, tweens[i]); + //Stretching any action that doesn't have the same length. + for (int i = 0; i < this.actions.length; i++) { + this.timeFactor[i] = 1; + if (this.actions[i].getLength() != length) { + double actionLength = this.actions[i].getLength(); + if (actionLength > 0 && length > 0) { + this.timeFactor[i] = this.actions[i].getLength() / length; + } } } - } - @Override - public float getWeightForTween(Tween tween) { + public void doInterpolate(double t) { blendWeight = blendSpace.getWeight(); - if (tween == firstActiveTween) { - return 1f; + BlendableAction firstActiveAction = (BlendableAction) actions[firstActiveIndex]; + BlendableAction secondActiveAction = (BlendableAction) actions[secondActiveIndex]; + firstActiveAction.setCollectTransformDelegate(this); + secondActiveAction.setCollectTransformDelegate(this); + + //only interpolate the first action if the weight if below 1. + if (blendWeight < 1f) { + firstActiveAction.setWeight(1f); + firstActiveAction.interpolate(t * timeFactor[firstActiveIndex]); + if (blendWeight == 0) { + for (HasLocalTransform target : targetMap.keySet()) { + collect(target, targetMap.get(target)); + } + } } - return weight * blendWeight; - } - @Override - public boolean doInterpolate(double t) { - if (firstActiveTween == null) { - blendSpace.getWeight(); - } + //Second action should be interpolated + secondActiveAction.setWeight(blendWeight); + secondActiveAction.interpolate(t * timeFactor[secondActiveIndex]); - boolean running = this.firstActiveTween.interpolate(t); - this.secondActiveTween.interpolate(t); + firstActiveAction.setCollectTransformDelegate(null); + secondActiveAction.setCollectTransformDelegate(null); - if (!running) { - return false; - } + } - return true; + protected Action[] getActions() { + return actions; } - @Override - public void reset() { + public BlendSpace getBlendSpace() { + return blendSpace; + } + protected void setFirstActiveIndex(int index) { + this.firstActiveIndex = index; } - protected Tween[] getTweens() { - return tweens; + protected void setSecondActiveIndex(int index) { + this.secondActiveIndex = index; } - public BlendSpace getBlendSpace() { - return blendSpace; + @Override + public Collection getTargets() { + return targetMap.keySet(); } - protected void setFirstActiveTween(Tween firstActiveTween) { - this.firstActiveTween = firstActiveTween; + @Override + public void collectTransform(HasLocalTransform target, Transform t, float weight, BlendableAction source) { + + Transform tr = targetMap.get(target); + if (weight == 1) { + tr.set(t); + } else if (weight > 0) { + tr.interpolateTransforms(tr, t, weight); + } + + if (source == actions[secondActiveIndex]) { + collect(target, tr); + } } - protected void setSecondActiveTween(Tween secondActiveTween) { - this.secondActiveTween = secondActiveTween; + private void collect(HasLocalTransform target, Transform tr) { + if (collectTransformDelegate != null) { + collectTransformDelegate.collectTransform(target, tr, this.weight, this); + } else { + if (getTransitionWeight() == 1) { + target.setLocalTransform(tr); + } else { + Transform trans = target.getLocalTransform(); + trans.interpolateTransforms(trans, tr, getTransitionWeight()); + target.setLocalTransform(trans); + } + } } + } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java new file mode 100644 index 000000000..6ad68edc2 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java @@ -0,0 +1,83 @@ +package com.jme3.anim.tween.action; + +import com.jme3.anim.tween.AbstractTween; +import com.jme3.anim.tween.Tween; +import com.jme3.anim.util.HasLocalTransform; +import com.jme3.math.Transform; + +import java.util.Collection; + +public abstract class BlendableAction extends Action { + + protected BlendableAction collectTransformDelegate; + private float transitionWeight = 1.0f; + private double transitionLength = 0.4f; + private TransitionTween transition = new TransitionTween(transitionLength); + + public BlendableAction(Tween... tweens) { + super(tweens); + } + + + public void setCollectTransformDelegate(BlendableAction delegate) { + this.collectTransformDelegate = delegate; + } + + @Override + public boolean interpolate(double t) { + // Sanity check the inputs + if (t < 0) { + return true; + } + + if (collectTransformDelegate == null) { + if (transition.getLength() > getLength()) { + transition.setLength(getLength()); + } + transition.interpolate(t); + } else { + transitionWeight = 1f; + } + + if (weight == 0) { + //weight is 0 let's not interpolate + return t < getLength(); + } + + doInterpolate(t); + + return t < getLength(); + } + + protected abstract void doInterpolate(double t); + + public abstract Collection getTargets(); + + public abstract void collectTransform(HasLocalTransform target, Transform t, float weight, BlendableAction source); + + public double getTransitionLength() { + return transitionLength; + } + + public void setTransitionLength(double transitionLength) { + this.transitionLength = transitionLength; + } + + protected float getTransitionWeight() { + return transitionWeight; + } + + private class TransitionTween extends AbstractTween { + + + public TransitionTween(double length) { + super(length); + } + + @Override + protected void doInterpolate(double t) { + transitionWeight = (float) t; + } + } + +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java new file mode 100644 index 000000000..2eb9aefc7 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java @@ -0,0 +1,64 @@ +package com.jme3.anim.tween.action; + +import com.jme3.anim.AnimClip; +import com.jme3.anim.TransformTrack; +import com.jme3.anim.tween.AbstractTween; +import com.jme3.anim.util.HasLocalTransform; +import com.jme3.math.Transform; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ClipAction extends BlendableAction { + + private AnimClip clip; + private Transform transform = new Transform(); + + public ClipAction(AnimClip clip) { + this.clip = clip; + length = clip.getLength(); + } + + @Override + public void doInterpolate(double t) { + TransformTrack[] tracks = clip.getTracks(); + for (TransformTrack track : tracks) { + HasLocalTransform target = track.getTarget(); + transform.set(target.getLocalTransform()); + track.getTransformAtTime(t, transform); + + if (collectTransformDelegate != null) { + collectTransformDelegate.collectTransform(target, transform, weight, this); + } else { + this.collectTransform(target, transform, getTransitionWeight(), this); + } + } + } + + public void reset() { + + } + + @Override + public Collection getTargets() { + List targets = new ArrayList<>(clip.getTracks().length); + for (TransformTrack track : clip.getTracks()) { + targets.add(track.getTarget()); + } + return targets; + } + + @Override + public void collectTransform(HasLocalTransform target, Transform t, float weight, BlendableAction source) { + if (weight == 1f) { + target.setLocalTransform(t); + } else { + Transform tr = target.getLocalTransform(); + tr.interpolateTransforms(tr, t, weight); + target.setLocalTransform(tr); + } + } + + +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java index 0c086d1e8..f9773f8a0 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java @@ -1,7 +1,5 @@ package com.jme3.anim.tween.action; -import com.jme3.anim.tween.Tween; - public class LinearBlendSpace implements BlendSpace { private BlendAction action; @@ -17,24 +15,24 @@ public class LinearBlendSpace implements BlendSpace { @Override public void setBlendAction(BlendAction action) { this.action = action; - Tween[] tweens = action.getTweens(); - step = maxValue / (float) (tweens.length - 1); + Action[] actions = action.getActions(); + step = maxValue / (float) (actions.length - 1); } @Override public float getWeight() { - Tween[] tweens = action.getTweens(); + Action[] actions = action.getActions(); float lowStep = 0, highStep = 0; int lowIndex = 0, highIndex = 0; - for (int i = 0; i < tweens.length && highStep < value; i++) { + for (int i = 0; i < actions.length && highStep < value; i++) { lowStep = highStep; lowIndex = i; highStep += step; } highIndex = lowIndex + 1; - action.setFirstActiveTween(tweens[lowIndex]); - action.setSecondActiveTween(tweens[highIndex]); + action.setFirstActiveIndex(lowIndex); + action.setSecondActiveIndex(highIndex); if (highStep == lowStep) { return 0; diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/SequenceAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/SequenceAction.java deleted file mode 100644 index 08c59085a..000000000 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/SequenceAction.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.jme3.anim.tween.action; - -import com.jme3.anim.tween.AbstractTween; -import com.jme3.anim.tween.Tween; - -public class SequenceAction extends Action { - - private int currentIndex = 0; - private double accumTime; - private double transitionTime = 0; - private float mainWeight = 1.0f; - private double transitionLength = 0.4f; - private TransitionTween transition = new TransitionTween(transitionLength); - - - public SequenceAction(Tween... tweens) { - super(tweens); - for (Tween tween : tweens) { - length += tween.getLength(); - } - } - - @Override - public float getWeightForTween(Tween tween) { - return weight * mainWeight; - } - - @Override - public boolean doInterpolate(double t) { - Tween currentTween = tweens[currentIndex]; - if (transition.getLength() > currentTween.getLength()) { - transition.setLength(currentTween.getLength()); - } - - transition.interpolate(t - transitionTime); - - boolean running = currentTween.interpolate(t - accumTime); - if (!running) { - accumTime += currentTween.getLength(); - currentIndex++; - transitionTime = accumTime; - transition.setLength(transitionLength); - } - - if (t >= length) { - reset(); - return false; - } - return true; - } - - public void reset() { - currentIndex = 0; - accumTime = 0; - transitionTime = 0; - mainWeight = 1; - } - - public void setTransitionLength(double transitionLength) { - this.transitionLength = transitionLength; - } - - private class TransitionTween extends AbstractTween { - - - public TransitionTween(double length) { - super(length); - } - - @Override - protected void doInterpolate(double t) { - mainWeight = (float) t; - } - } -} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index a2c40dd60..d3eea6de5 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -3,6 +3,7 @@ package jme3test.model.anim; import com.jme3.anim.AnimComposer; import com.jme3.anim.SkinningControl; import com.jme3.anim.tween.action.BlendAction; +import com.jme3.anim.tween.action.BlendableAction; import com.jme3.anim.tween.action.LinearBlendSpace; import com.jme3.anim.util.AnimMigrationUtils; import com.jme3.app.ChaseCameraAppState; @@ -159,13 +160,12 @@ public class TestAnimMigration extends SimpleApplication { anims.add(name); } composer.actionSequence("Sequence", - composer.tweenFromClip("Walk"), - composer.tweenFromClip("Run"), - composer.tweenFromClip("Jumping")); + composer.action("Walk"), + composer.action("Run"), + composer.action("Jumping")); action = composer.actionBlended("Blend", new LinearBlendSpace(4), - composer.tweenFromClip("Walk"), - composer.tweenFromClip("Jumping")); + "Walk", "Punches", "Jumping", "Taunt"); action.getBlendSpace().setValue(2); From 8634509a95ca4147e5b37014a64ff7dcfff57755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81my=20Bouquet?= Date: Tue, 2 Jan 2018 13:34:12 +0100 Subject: [PATCH 22/54] Speed support and some clean up --- .../main/java/com/jme3/anim/AnimComposer.java | 31 ++++++++++++++----- .../com/jme3/anim/tween/action/Action.java | 22 ++++++++++--- .../jme3/anim/tween/action/BaseAction.java | 11 ++----- .../jme3/anim/tween/action/BlendAction.java | 12 +++---- .../jme3/anim/tween/action/BlendSpace.java | 2 -- .../anim/tween/action/BlendableAction.java | 11 ++++++- .../jme3/anim/tween/action/ClipAction.java | 4 +-- .../anim/tween/action/LinearBlendSpace.java | 9 +++--- .../model/anim/TestAnimMigration.java | 23 ++++++++------ 9 files changed, 80 insertions(+), 45 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index 1814faf72..6a8f92304 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -21,6 +21,7 @@ public class AnimComposer extends AbstractControl { private Action currentAction; private Map actions = new HashMap<>(); + private float globalSpeed = 1f; private float time; /** @@ -66,16 +67,22 @@ public class AnimComposer extends AbstractControl { public Action action(String name) { Action action = actions.get(name); if (action == null) { - AnimClip clip = animClipMap.get(name); - if (clip == null) { - throw new IllegalArgumentException("Cannot find clip named " + name); - } - action = new ClipAction(clip); + action = makeAction(name); actions.put(name, action); } return action; } + public Action makeAction(String name) { + Action action; + AnimClip clip = animClipMap.get(name); + if (clip == null) { + throw new IllegalArgumentException("Cannot find clip named " + name); + } + action = new ClipAction(clip); + return action; + } + public BaseAction actionSequence(String name, Tween... tweens) { BaseAction action = new BaseAction(Tweens.sequence(tweens)); @@ -86,7 +93,7 @@ public class AnimComposer extends AbstractControl { public BlendAction actionBlended(String name, BlendSpace blendSpace, String... clips) { BlendableAction[] acts = new BlendableAction[clips.length]; for (int i = 0; i < acts.length; i++) { - BlendableAction ba = (BlendableAction) action(clips[i]); + BlendableAction ba = (BlendableAction) makeAction(clips[i]); acts[i] = ba; } BlendAction action = new BlendAction(blendSpace, acts); @@ -111,9 +118,9 @@ public class AnimComposer extends AbstractControl { protected void controlUpdate(float tpf) { if (currentAction != null) { time += tpf; - boolean running = currentAction.interpolate(time); + boolean running = currentAction.interpolate(time * globalSpeed); if (!running) { - time -= currentAction.getLength(); + time = 0; } } } @@ -123,6 +130,14 @@ public class AnimComposer extends AbstractControl { } + public float getGlobalSpeed() { + return globalSpeed; + } + + public void setGlobalSpeed(float globalSpeed) { + this.globalSpeed = globalSpeed; + } + @Override public Object jmeClone() { try { diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java index d445abf09..21d5bdc62 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java @@ -5,8 +5,8 @@ import com.jme3.anim.tween.Tween; public abstract class Action implements Tween { protected Action[] actions; - protected float weight = 1; - protected double length; + private double length; + private double speed = 1; protected Action(Tween... tweens) { this.actions = new Action[tweens.length]; @@ -20,13 +20,27 @@ public abstract class Action implements Tween { } } + @Override + public boolean interpolate(double t) { + return subInterpolate(t * speed); + } + + public abstract boolean subInterpolate(double t); + @Override public double getLength() { return length; } - public void setWeight(float weight) { - this.weight = weight; + protected void setLength(double length) { + this.length = length; } + public double getSpeed() { + return speed; + } + + public void setSpeed(double speed) { + this.speed = speed; + } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java index 6e59ce091..c3c021530 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java @@ -11,7 +11,7 @@ public class BaseAction extends Action { public BaseAction(Tween tween) { this.tween = tween; - length = tween.getLength(); + setLength(tween.getLength()); gatherActions(tween); } @@ -27,14 +27,7 @@ public class BaseAction extends Action { } @Override - public void setWeight(float weight) { - for (Action action : subActions.getArray()) { - action.setWeight(weight); - } - } - - @Override - public boolean interpolate(double t) { + public boolean subInterpolate(double t) { return tween.interpolate(t); } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java index 97188a11f..9c5848e4d 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java @@ -23,8 +23,8 @@ public class BlendAction extends BlendableAction { blendSpace.setBlendAction(this); for (BlendableAction action : actions) { - if (action.getLength() > length) { - length = action.getLength(); + if (action.getLength() > getLength()) { + setLength(action.getLength()); } Collection targets = action.getTargets(); for (HasLocalTransform target : targets) { @@ -40,10 +40,10 @@ public class BlendAction extends BlendableAction { //Stretching any action that doesn't have the same length. for (int i = 0; i < this.actions.length; i++) { this.timeFactor[i] = 1; - if (this.actions[i].getLength() != length) { + if (this.actions[i].getLength() != getLength()) { double actionLength = this.actions[i].getLength(); - if (actionLength > 0 && length > 0) { - this.timeFactor[i] = this.actions[i].getLength() / length; + if (actionLength > 0 && getLength() > 0) { + this.timeFactor[i] = this.actions[i].getLength() / getLength(); } } } @@ -114,7 +114,7 @@ public class BlendAction extends BlendableAction { private void collect(HasLocalTransform target, Transform tr) { if (collectTransformDelegate != null) { - collectTransformDelegate.collectTransform(target, tr, this.weight, this); + collectTransformDelegate.collectTransform(target, tr, this.getWeight(), this); } else { if (getTransitionWeight() == 1) { target.setLocalTransform(tr); diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java index 956e74c3b..a88be7529 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java @@ -1,7 +1,5 @@ package com.jme3.anim.tween.action; -import com.jme3.anim.tween.action.BlendAction; - public interface BlendSpace { public void setBlendAction(BlendAction action); diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java index 6ad68edc2..f1bcef9f2 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java @@ -12,6 +12,7 @@ public abstract class BlendableAction extends Action { protected BlendableAction collectTransformDelegate; private float transitionWeight = 1.0f; private double transitionLength = 0.4f; + private float weight = 1f; private TransitionTween transition = new TransitionTween(transitionLength); public BlendableAction(Tween... tweens) { @@ -24,7 +25,7 @@ public abstract class BlendableAction extends Action { } @Override - public boolean interpolate(double t) { + public boolean subInterpolate(double t) { // Sanity check the inputs if (t < 0) { return true; @@ -49,6 +50,14 @@ public abstract class BlendableAction extends Action { return t < getLength(); } + public float getWeight() { + return weight; + } + + public void setWeight(float weight) { + this.weight = weight; + } + protected abstract void doInterpolate(double t); public abstract Collection getTargets(); diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java index 2eb9aefc7..23cc9743e 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java @@ -17,7 +17,7 @@ public class ClipAction extends BlendableAction { public ClipAction(AnimClip clip) { this.clip = clip; - length = clip.getLength(); + setLength(clip.getLength()); } @Override @@ -29,7 +29,7 @@ public class ClipAction extends BlendableAction { track.getTransformAtTime(t, transform); if (collectTransformDelegate != null) { - collectTransformDelegate.collectTransform(target, transform, weight, this); + collectTransformDelegate.collectTransform(target, transform, getWeight(), this); } else { this.collectTransform(target, transform, getTransitionWeight(), this); } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java index f9773f8a0..31d2931fe 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java @@ -5,24 +5,25 @@ public class LinearBlendSpace implements BlendSpace { private BlendAction action; private float value; private float maxValue; + private float minValue; private float step; - public LinearBlendSpace(float maxValue) { + public LinearBlendSpace(float minValue, float maxValue) { this.maxValue = maxValue; - + this.minValue = minValue; } @Override public void setBlendAction(BlendAction action) { this.action = action; Action[] actions = action.getActions(); - step = maxValue / (float) (actions.length - 1); + step = (maxValue - minValue) / (float) (actions.length - 1); } @Override public float getWeight() { Action[] actions = action.getActions(); - float lowStep = 0, highStep = 0; + float lowStep = minValue, highStep = minValue; int lowIndex = 0, highIndex = 0; for (int i = 0; i < actions.length && highStep < value; i++) { lowStep = highStep; diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index d3eea6de5..9a3782676 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -2,6 +2,7 @@ package jme3test.model.anim; import com.jme3.anim.AnimComposer; import com.jme3.anim.SkinningControl; +import com.jme3.anim.tween.action.Action; import com.jme3.anim.tween.action.BlendAction; import com.jme3.anim.tween.action.BlendableAction; import com.jme3.anim.tween.action.LinearBlendSpace; @@ -32,7 +33,7 @@ public class TestAnimMigration extends SimpleApplication { LinkedList anims = new LinkedList<>(); boolean playAnim = false; BlendAction action; - float blendValue = 2f; + float blendValue = 1f; public static void main(String... argv) { TestAnimMigration app = new TestAnimMigration(); @@ -132,13 +133,15 @@ public class TestAnimMigration extends SimpleApplication { public void onAnalog(String name, float value, float tpf) { if (name.equals("blendUp")) { blendValue += value; - blendValue = FastMath.clamp(blendValue, 0, 4); + blendValue = FastMath.clamp(blendValue, 1, 4); action.getBlendSpace().setValue(blendValue); + action.setSpeed(blendValue); } if (name.equals("blendDown")) { blendValue -= value; - blendValue = FastMath.clamp(blendValue, 0, 4); + blendValue = FastMath.clamp(blendValue, 1, 4); action.getBlendSpace().setValue(blendValue); + action.setSpeed(blendValue); } System.err.println(blendValue); } @@ -160,14 +163,16 @@ public class TestAnimMigration extends SimpleApplication { anims.add(name); } composer.actionSequence("Sequence", - composer.action("Walk"), - composer.action("Run"), - composer.action("Jumping")); + composer.makeAction("Walk"), + composer.makeAction("Run"), + composer.makeAction("Jumping")).setSpeed(4); - action = composer.actionBlended("Blend", new LinearBlendSpace(4), - "Walk", "Punches", "Jumping", "Taunt"); + action = composer.actionBlended("Blend", new LinearBlendSpace(1, 4), + "Walk", "Run"); - action.getBlendSpace().setValue(2); + action.getBlendSpace().setValue(1); + + composer.action("Walk").setSpeed(2); // composer.actionSequence("Sequence", // composer.tweenFromClip("Walk"), From 2e323eb7cf8427658b8f2239ee3c68496ef71036 Mon Sep 17 00:00:00 2001 From: Nehon Date: Sun, 7 Jan 2018 14:33:11 +0100 Subject: [PATCH 23/54] fixes some isues with the gltfLoader when there are several skins --- .../java/jme3test/model/TestGltfLoading.java | 6 +++-- .../jme3/scene/plugins/gltf/GltfLoader.java | 22 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index 06ec75f5f..f9f818f3c 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -109,6 +109,8 @@ public class TestGltfLoading extends SimpleApplication { // rootNode.addLight(pl); // PointLight pl1 = new PointLight(new Vector3f(-5.0f, -5.0f, -5.0f), ColorRGBA.White.mult(0.5f), 50); // rootNode.addLight(pl1); + + loadModel("Models/gltf/polly/project_polly.gltf", new Vector3f(0, 0, 0), 0.5f); //loadModel("Models/gltf/nier/scene.gltf", new Vector3f(0, -1.5f, 0), 0.01f); //loadModel("Models/gltf/izzy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/darth/scene.gltf", new Vector3f(0, -1, 0), 0.01f); @@ -118,7 +120,7 @@ public class TestGltfLoading extends SimpleApplication { //loadModel("Models/gltf/war/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/ganjaarl/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/hero/scene.gltf", new Vector3f(0, -1, 0), 0.1f); - loadModel("Models/gltf/mercy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + /// loadModel("Models/gltf/mercy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/crab/scene.gltf", Vector3f.ZERO, 1); //loadModel("Models/gltf/manta/scene.gltf", Vector3f.ZERO, 0.2f); //loadModel("Models/gltf/bone/scene.gltf", Vector3f.ZERO, 0.1f); @@ -234,7 +236,7 @@ public class TestGltfLoading extends SimpleApplication { } //System.err.println(ctrl.getArmature().toString()); //ctrl.setHardwareSkinningPreferred(false); - getStateManager().getState(ArmatureDebugAppState.class).addArmatureFrom(ctrl); + // getStateManager().getState(ArmatureDebugAppState.class).addArmatureFrom(ctrl); // AnimControl aCtrl = findControl(s, AnimControl.class); // //ctrl.getSpatial().removeControl(ctrl); // if (aCtrl == null) { 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 d77675e27..a6ff39d2e 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 @@ -245,9 +245,11 @@ public class GltfLoader implements AssetLoader { Integer skinIndex = getAsInteger(nodeData, "skin"); if (skinIndex != null) { SkinData skinData = fetchFromCache("skins", skinIndex, SkinData.class); - List spatials = skinnedSpatials.get(skinData); - spatials.add(spatial); - skinData.used = true; + if (skinData != null) { + List spatials = skinnedSpatials.get(skinData); + spatials.add(spatial); + skinData.used = true; + } } spatial.setLocalTransform(readTransforms(nodeData)); @@ -1027,20 +1029,22 @@ public class GltfLoader implements AssetLoader { private void setupControls() { for (SkinData skinData : skinnedSpatials.keySet()) { List spatials = skinnedSpatials.get(skinData); - Spatial spatial = skinData.parent; if (spatials.isEmpty()) { continue; } + Spatial spatial = skinData.parent; if (spatials.size() >= 1) { spatial = findCommonAncestor(spatials); } - if (skinData.parent != null && spatial != skinData.parent) { - skinData.rootBoneTransformOffset = spatial.getWorldTransform().invert(); - skinData.rootBoneTransformOffset.combineWithParent(skinData.parent.getWorldTransform()); - } - +// if (spatial != skinData.parent) { +// skinData.rootBoneTransformOffset = spatial.getWorldTransform().invert(); +// if (skinData.parent != null) { +// skinData.rootBoneTransformOffset.combineWithParent(skinData.parent.getWorldTransform()); +// } +// } + if (skinData.animComposer != null && skinData.animComposer.getSpatial() == null) { spatial.addControl(skinData.animComposer); } From 072bdbb6be3f843f41260c5155d04261bca97eca Mon Sep 17 00:00:00 2001 From: Nehon Date: Wed, 31 Jan 2018 21:25:24 +0100 Subject: [PATCH 24/54] Enhanced hardware skinning test --- .../jme3test/model/anim/TestHWSkinning.java | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java index 25544e8cc..9862ebffa 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java @@ -33,6 +33,7 @@ package jme3test.model.anim; import com.jme3.anim.AnimComposer; import com.jme3.anim.SkinningControl; +import com.jme3.app.DetailedProfilerState; import com.jme3.app.SimpleApplication; import com.jme3.font.BitmapText; import com.jme3.input.KeyInput; @@ -40,6 +41,7 @@ import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.light.DirectionalLight; import com.jme3.math.*; +import com.jme3.scene.Node; import com.jme3.scene.Spatial; import java.util.ArrayList; @@ -50,7 +52,7 @@ public class TestHWSkinning extends SimpleApplication implements ActionListener{ // private AnimComposer composer; private String[] animNames = {"Dodge", "Walk", "pull", "push"}; - private final static int SIZE = 40; + private final static int SIZE = 60; private boolean hwSkinningEnable = true; private List skControls = new ArrayList(); private BitmapText hwsText; @@ -65,8 +67,8 @@ public class TestHWSkinning extends SimpleApplication implements ActionListener{ flyCam.setMoveSpeed(10f); flyCam.setDragToRotate(true); setPauseOnLostFocus(false); - cam.setLocation(new Vector3f(24.746134f, 13.081396f, 32.72753f)); - cam.setRotation(new Quaternion(-0.06867662f, 0.92435044f, -0.19981281f, -0.31770203f)); + cam.setLocation(new Vector3f(38.76639f, 14.744472f, 45.097454f)); + cam.setRotation(new Quaternion(-0.06086266f, 0.92303723f, -0.1639443f, -0.34266636f)); makeHudText(); @@ -75,23 +77,38 @@ public class TestHWSkinning extends SimpleApplication implements ActionListener{ dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f)); rootNode.addLight(dl); + Spatial models[] = new Spatial[4]; + for (int i = 0; i < 4; i++) { + models[i] =loadModel(i); + } + for (int i = 0; i < SIZE; i++) { for (int j = 0; j < SIZE; j++) { - Spatial model = (Spatial) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); - model.setLocalScale(0.1f); - model.setLocalTranslation(i - SIZE / 2, 0, j - SIZE / 2); - AnimComposer composer = model.getControl(AnimComposer.class); - - composer.setCurrentAction(animNames[(i + j) % 4]); - SkinningControl skinningControl = model.getControl(SkinningControl.class); - skinningControl.setHardwareSkinningPreferred(hwSkinningEnable); - skControls.add(skinningControl); - rootNode.attachChild(model); + Node model = (Node)models[(i + j) % 4]; + Spatial s = model.getChild(0).clone(); + model.attachChild(s); + float x = (float)(i - SIZE / 2) / 0.1f; + float z = (float)(j - SIZE / 2) / 0.1f; + s.setLocalTranslation(x, 0, z); } } inputManager.addListener(this, "toggleHWS"); inputManager.addMapping("toggleHWS", new KeyTrigger(KeyInput.KEY_SPACE)); + + } + + private Spatial loadModel(int i) { + Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + model.setLocalScale(0.1f); + AnimComposer composer = model.getControl(AnimComposer.class); + + composer.setCurrentAction(animNames[i]); + SkinningControl skinningControl = model.getControl(SkinningControl.class); + skinningControl.setHardwareSkinningPreferred(hwSkinningEnable); + skControls.add(skinningControl); + rootNode.attachChild(model); + return model; } @Override From 829354d990930d76e3fed9337e6251f3ea69f269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81my=20Bouquet?= Date: Mon, 26 Feb 2018 16:38:00 +0100 Subject: [PATCH 25/54] Better armature debugger --- .../debug/custom/ArmatureDebugAppState.java | 24 +++++++++---------- .../scene/debug/custom/ArmatureDebugger.java | 8 +------ .../jme3/scene/debug/custom/ArmatureNode.java | 16 ++++++++++--- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java index fb16135e6..a3618b444 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java @@ -30,6 +30,8 @@ public class ArmatureDebugAppState extends BaseAppState { private Application app; private boolean displayAllJoints = false; private float clickDelay = -1; + Vector3f tmp = new Vector3f(); + Vector3f tmp2 = new Vector3f(); ViewPort vp; @Override @@ -84,10 +86,15 @@ public class ArmatureDebugAppState extends BaseAppState { public ArmatureDebugger addArmatureFrom(Armature armature, Spatial forSpatial) { + ArmatureDebugger ad = armatures.get(armature); + if(ad != null){ + return ad; + } + JointInfoVisitor visitor = new JointInfoVisitor(armature); forSpatial.depthFirstTraversal(visitor); - ArmatureDebugger ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature, visitor.deformingJoints); + ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature, visitor.deformingJoints); ad.setLocalTransform(forSpatial.getWorldTransform()); if (forSpatial instanceof Node) { List geoms = new ArrayList<>(); @@ -122,18 +129,11 @@ public class ArmatureDebugAppState extends BaseAppState { if (name.equals("shoot") && !isPressed && clickDelay < CLICK_MAX_DELAY) { Vector2f click2d = app.getInputManager().getCursorPosition(); CollisionResults results = new CollisionResults(); - //first check 2d collision with joints - for (ArmatureDebugger ad : armatures.values()) { - ad.pick(click2d, results); - } - if (results.size() == 0) { - //no result, let's ray cast for bone geometries - Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); - Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); - Ray ray = new Ray(click3d, dir); - debugNode.collideWith(ray, results); - } + Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f, tmp); + Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f, tmp2).subtractLocal(click3d); + Ray ray = new Ray(click3d, dir); + debugNode.collideWith(ray, results); if (results.size() == 0) { for (ArmatureDebugger ad : armatures.values()) { diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java index ac93bfaaf..31c292186 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java @@ -121,7 +121,7 @@ public class ArmatureDebugger extends Node { ((Node) wires.getChild(1)).getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always); } - protected void initialize(AssetManager assetManager, Camera camera) { + public void initialize(AssetManager assetManager, Camera camera) { armatureNode.setCamera(camera); @@ -153,10 +153,6 @@ public class ArmatureDebugger extends Node { } - public int pick(Vector2f cursor, CollisionResults results) { - return armatureNode.pick(cursor, results); - } - public Armature getArmature() { return armature; } @@ -169,9 +165,7 @@ public class ArmatureDebugger extends Node { @Override public int collideWith(Collidable other, CollisionResults results) { - return armatureNode.collideWith(other, results); - } protected Joint select(Geometry g) { diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java index 7f8b26c2d..038936049 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java @@ -62,6 +62,7 @@ public class ArmatureNode extends Node { private Map geomToJoint = new HashMap<>(); private Joint selectedJoint = null; private Vector3f tmp = new Vector3f(); + private Vector2f tmpv2 = new Vector2f(); private final static ColorRGBA selectedColor = ColorRGBA.Orange; private final static ColorRGBA selectedColorJ = ColorRGBA.Yellow; private final static ColorRGBA outlineColor = ColorRGBA.LightGray; @@ -262,7 +263,16 @@ public class ArmatureNode extends Node { if (!(other instanceof Ray)) { return 0; } - int nbCol = 0; + + // first try a 2D pick; + camera.getScreenCoordinates(((Ray)other).getOrigin(),tmp); + tmpv2.x = tmp.x; + tmpv2.y = tmp.y; + int nbHit = pick(tmpv2, results); + if (nbHit > 0) { + return nbHit; + } + for (Geometry g : geomToJoint.keySet()) { if (g.getMesh() instanceof JointShape) { continue; @@ -275,11 +285,11 @@ public class ArmatureNode extends Node { CollisionResult res = new CollisionResult(); res.setGeometry(g); results.addCollision(res); - nbCol++; + nbHit++; } } } - return nbCol; + return nbHit; } private void updateBoneMesh(Geometry geom, Vector3f start, Vector3f[] ends) { From d9a8666742b22129565b18a880775b775ec901c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81my=20Bouquet?= Date: Mon, 26 Feb 2018 16:38:35 +0100 Subject: [PATCH 26/54] Fixes link to original paper in shadow renderer --- .../java/com/jme3/shadow/DirectionalLightShadowRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/shadow/DirectionalLightShadowRenderer.java b/jme3-core/src/main/java/com/jme3/shadow/DirectionalLightShadowRenderer.java index f1b34cf19..e203f16a9 100644 --- a/jme3-core/src/main/java/com/jme3/shadow/DirectionalLightShadowRenderer.java +++ b/jme3-core/src/main/java/com/jme3/shadow/DirectionalLightShadowRenderer.java @@ -57,7 +57,7 @@ import java.io.IOException; * are from the camera, the smaller they are to maximize the resolution used of * the shadow map.
This results in a better quality shadow than standard * shadow mapping.
for more informations on this read this
http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html
+ * href="https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html">https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html
*

* @author Rémy Bouquet aka Nehon */ @@ -83,7 +83,7 @@ public class DirectionalLightShadowRenderer extends AbstractShadowRenderer { /** * Create a DirectionalLightShadowRenderer More info on the technique at http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html + * href="https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html">https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html * * @param assetManager the application asset manager * @param shadowMapSize the size of the rendered shadowmaps (512,1024,2048, From 42215f489036fd5fdeec4a33e91ecc4cdc3c97e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81my=20Bouquet?= Date: Mon, 26 Feb 2018 16:38:57 +0100 Subject: [PATCH 27/54] Hardware Morph animation implementation and glTF loading --- .../src/main/java/com/jme3/anim/AnimClip.java | 10 +- .../main/java/com/jme3/anim/AnimTrack.java | 12 + .../main/java/com/jme3/anim/MorphControl.java | 165 +++++++++++++ .../main/java/com/jme3/anim/MorphTrack.java | 217 ++++++++++++++++++ .../java/com/jme3/anim/TransformTrack.java | 13 +- .../src/main/java/com/jme3/anim/Weights.java | 95 ++++++++ .../anim/interpolator/AnimInterpolators.java | 2 - .../anim/interpolator/FrameInterpolator.java | 15 ++ .../jme3/anim/tween/action/ClipAction.java | 51 ++-- .../java/com/jme3/animation/CompactArray.java | 6 +- .../com/jme3/animation/CompactFloatArray.java | 100 ++++++++ .../src/main/java/com/jme3/scene/Mesh.java | 127 ++++++++-- .../java/com/jme3/scene/VertexBuffer.java | 66 +++++- .../java/com/jme3/scene/mesh/MorphTarget.java | 40 ++++ .../Common/MatDefs/Light/Lighting.j3md | 17 ++ .../Common/MatDefs/Light/Lighting.vert | 10 + .../Common/MatDefs/Light/PBRLighting.j3md | 17 ++ .../Common/MatDefs/Light/PBRLighting.vert | 17 +- .../Common/MatDefs/Light/SPLighting.vert | 10 + .../Common/MatDefs/Misc/Unshaded.j3md | 49 ++-- .../Common/MatDefs/Misc/Unshaded.vert | 6 + .../ShaderNodes/Common/FixedScale100.frag | 2 +- .../Common/MatDefs/Shadow/PostShadow.vert | 7 +- .../Common/MatDefs/Shadow/PreShadow.vert | 8 +- .../Common/ShaderLib/MorphAnim.glsllib | 212 +++++++++++++++++ .../java/jme3test/model/TestGltfLoading.java | 44 +++- .../jme3test/model/anim/TestHWSkinning.java | 4 +- .../java/jme3test/model/anim/TestMorph.java | 121 ++++++++++ .../jme3/scene/plugins/gltf/GltfLoader.java | 112 +++++++-- .../jme3/scene/plugins/gltf/GltfUtils.java | 2 + .../jme3/scene/plugins/gltf/TrackData.java | 13 +- 31 files changed, 1442 insertions(+), 128 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/anim/AnimTrack.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/MorphControl.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/MorphTrack.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/Weights.java create mode 100644 jme3-core/src/main/java/com/jme3/animation/CompactFloatArray.java create mode 100644 jme3-core/src/main/java/com/jme3/scene/mesh/MorphTarget.java create mode 100644 jme3-core/src/main/resources/Common/ShaderLib/MorphAnim.glsllib create mode 100644 jme3-examples/src/main/java/jme3test/model/anim/TestMorph.java diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java index 96cde46a2..0baf7b873 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java @@ -16,7 +16,7 @@ public class AnimClip implements JmeCloneable, Savable { private String name; private double length; - private TransformTrack[] tracks; + private AnimTrack[] tracks; public AnimClip() { } @@ -25,9 +25,9 @@ public class AnimClip implements JmeCloneable, Savable { this.name = name; } - public void setTracks(TransformTrack[] tracks) { + public void setTracks(AnimTrack[] tracks) { this.tracks = tracks; - for (TransformTrack track : tracks) { + for (AnimTrack track : tracks) { if (track.getLength() > length) { length = track.getLength(); } @@ -44,7 +44,7 @@ public class AnimClip implements JmeCloneable, Savable { } - public TransformTrack[] getTracks() { + public AnimTrack[] getTracks() { return tracks; } @@ -59,7 +59,7 @@ public class AnimClip implements JmeCloneable, Savable { @Override public void cloneFields(Cloner cloner, Object original) { - TransformTrack[] newTracks = new TransformTrack[tracks.length]; + AnimTrack[] newTracks = new AnimTrack[tracks.length]; for (int i = 0; i < tracks.length; i++) { newTracks[i] = (cloner.clone(tracks[i])); } diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimTrack.java b/jme3-core/src/main/java/com/jme3/anim/AnimTrack.java new file mode 100644 index 000000000..45b54cf7f --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/AnimTrack.java @@ -0,0 +1,12 @@ +package com.jme3.anim; + +import com.jme3.export.Savable; +import com.jme3.util.clone.JmeCloneable; + +public interface AnimTrack extends Savable, JmeCloneable { + + public void getDataAtTime(double time, T store); + public double getLength(); + + +} diff --git a/jme3-core/src/main/java/com/jme3/anim/MorphControl.java b/jme3-core/src/main/java/com/jme3/anim/MorphControl.java new file mode 100644 index 000000000..31abc8c9c --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/MorphControl.java @@ -0,0 +1,165 @@ +package com.jme3.anim; + +import com.jme3.material.*; +import com.jme3.renderer.*; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.SceneGraphVisitorAdapter; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.control.AbstractControl; +import com.jme3.scene.mesh.MorphTarget; +import com.jme3.shader.VarType; +import com.jme3.util.SafeArrayList; + +import java.nio.FloatBuffer; + +/** + * A control that handle morph animation for Position, Normal and Tangent buffers. + * All stock shaders only support morphing these 3 buffers, but note that MorphTargets can have any type of buffers. + * If you want to use other types of buffers you will need a custom MorphControl and a custom shader. + * @author Rémy Bouquet + */ +public class MorphControl extends AbstractControl { + + private static final int MAX_MORPH_BUFFERS = 14; + private final static float MIN_WEIGHT = 0.005f; + + private SafeArrayList targets = new SafeArrayList<>(Geometry.class); + private TargetLocator targetLocator = new TargetLocator(); + + private boolean approximateTangents = true; + private MatParamOverride nullNumberOfBones = new MatParamOverride(VarType.Int, "NumberOfBones", null); + + @Override + protected void controlUpdate(float tpf) { + // gathering geometries in the sub graph. + // This must be done in the update phase as the gathering might add a matparam override + targets.clear(); + this.spatial.depthFirstTraversal(targetLocator); + } + + @Override + protected void controlRender(RenderManager rm, ViewPort vp) { + for (Geometry target : targets) { + Mesh mesh = target.getMesh(); + if (!mesh.isDirtyMorph()) { + continue; + } + int nbMaxBuffers = getRemainingBuffers(mesh, rm.getRenderer()); + Material m = target.getMaterial(); + + float weights[] = mesh.getMorphState(); + MorphTarget morphTargets[] = mesh.getMorphTargets(); + float matWeights[]; + MatParam param = m.getParam("MorphWeights"); + + //Number of buffer to handle for each morph target + int targetNumBuffers = getTargetNumBuffers(morphTargets[0]); + // compute the max number of targets to send to the GPU + int maxGPUTargets = Math.min(nbMaxBuffers, MAX_MORPH_BUFFERS) / targetNumBuffers; + if (param == null) { + matWeights = new float[maxGPUTargets]; + m.setParam("MorphWeights", VarType.FloatArray, matWeights); + } else { + matWeights = (float[]) param.getValue(); + } + + // setting the maximum number as the real number may change every frame and trigger a shader recompilation since it's bound to a define. + m.setInt("NumberOfMorphTargets", maxGPUTargets); + m.setInt("NumberOfTargetsBuffers", targetNumBuffers); + + int nbGPUTargets = 0; + int nbCPUBuffers = 0; + int boundBufferIdx = 0; + for (int i = 0; i < morphTargets.length; i++) { + if (weights[i] < MIN_WEIGHT) { + continue; + } + if (nbGPUTargets >= maxGPUTargets) { + //TODO we should fallback to CPU there. + nbCPUBuffers++; + continue; + } + int start = VertexBuffer.Type.MorphTarget0.ordinal(); + MorphTarget t = morphTargets[i]; + if (targetNumBuffers >= 1) { + activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Position)); + boundBufferIdx++; + } + if (targetNumBuffers >= 2) { + activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Normal)); + boundBufferIdx++; + } + if (!approximateTangents && targetNumBuffers == 3) { + activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Tangent)); + boundBufferIdx++; + } + matWeights[nbGPUTargets] = weights[i]; + nbGPUTargets++; + + } + if (nbGPUTargets < matWeights.length) { + for (int i = nbGPUTargets; i < matWeights.length; i++) { + matWeights[i] = 0; + } + } + } + } + + private void activateBuffer(Mesh mesh, int idx, int start, FloatBuffer b) { + mesh.setBuffer(VertexBuffer.Type.values()[start + idx], 3, b); + } + + private int getTargetNumBuffers(MorphTarget morphTarget) { + int num = 0; + if (morphTarget.getBuffer(VertexBuffer.Type.Position) != null) num++; + if (morphTarget.getBuffer(VertexBuffer.Type.Normal) != null) num++; + + // if tangents are not needed we don't count the tangent buffer + if (!approximateTangents && morphTarget.getBuffer(VertexBuffer.Type.Tangent) != null) { + num++; + } + return num; + } + + private int getRemainingBuffers(Mesh mesh, Renderer renderer) { + int nbUsedBuffers = 0; + for (VertexBuffer vb : mesh.getBufferList().getArray()) { + boolean isMorphBuffer = vb.getBufferType().ordinal() >= VertexBuffer.Type.MorphTarget0.ordinal() && vb.getBufferType().ordinal() <= VertexBuffer.Type.MorphTarget9.ordinal(); + if (vb.getBufferType() == VertexBuffer.Type.Index || isMorphBuffer) continue; + if (vb.getUsage() != VertexBuffer.Usage.CpuOnly) { + nbUsedBuffers++; + } + } + return renderer.getLimits().get(Limits.VertexAttributes) - nbUsedBuffers; + } + + public void setApproximateTangents(boolean approximateTangents) { + this.approximateTangents = approximateTangents; + } + + public boolean isApproximateTangents() { + return approximateTangents; + } + + private class TargetLocator extends SceneGraphVisitorAdapter { + @Override + public void visit(Geometry geom) { + MatParam p = geom.getMaterial().getMaterialDef().getMaterialParam("MorphWeights"); + if (p == null) { + return; + } + Mesh mesh = geom.getMesh(); + if (mesh != null && mesh.hasMorphTargets()) { + targets.add(geom); + // If the mesh is in a subgraph of a node with a SkinningControl it might have hardware skinning activated through mat param override even if it's not skinned. + // this code makes sure that if the mesh has no hardware skinning buffers hardware skinning won't be activated. + // this is important, because if HW skinning is activated the shader will declare 2 additional useless attributes, + // and we desperately need all the attributes we can find for Morph animation. + if (mesh.getBuffer(VertexBuffer.Type.HWBoneIndex) == null && !geom.getLocalMatParamOverrides().contains(nullNumberOfBones)) { + geom.addMatParamOverride(nullNumberOfBones); + } + } + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java b/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java new file mode 100644 index 000000000..b1968df36 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java @@ -0,0 +1,217 @@ +/* + * 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.anim; + +import com.jme3.anim.interpolator.FrameInterpolator; +import com.jme3.animation.*; +import com.jme3.export.*; +import com.jme3.scene.Geometry; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; + +/** + * Contains a list of weights and times for each keyframe. + * + * @author Rémy Bouquet + */ +public class MorphTrack implements AnimTrack { + + private double length; + private Geometry target; + + /** + * Weights and times for track. + */ + private float[] weights; + private FrameInterpolator interpolator = FrameInterpolator.DEFAULT; + private float[] times; + private int nbMorphTargets; + + /** + * Serialization-only. Do not use. + */ + public MorphTrack() { + } + + /** + * Creates a morph track with the given Geometry as a target + * + * @param times a float array with the time of each frame + * @param weights the morphs for each frames + */ + public MorphTrack(Geometry target, float[] times, float[] weights, int nbMorphTargets) { + this.target = target; + this.nbMorphTargets = nbMorphTargets; + this.setKeyframes(times, weights); + } + + /** + * return the array of weights of this track + * + * @return + */ + public float[] getWeights() { + return weights; + } + + /** + * returns the arrays of time for this track + * + * @return + */ + public float[] getTimes() { + return times; + } + + /** + * Sets the keyframes times for this Joint track + * + * @param times the keyframes times + */ + public void setTimes(float[] times) { + if (times.length == 0) { + throw new RuntimeException("TransformTrack with no keyframes!"); + } + this.times = times; + length = times[times.length - 1] - times[0]; + } + + + /** + * Set the weight for this morph track + * + * @param times a float array with the time of each frame + * @param weights the weights of the morphs for each frame + + */ + public void setKeyframes(float[] times, float[] weights) { + setTimes(times); + if (weights != null) { + if (times == null) { + throw new RuntimeException("MorphTrack doesn't have any time for key frames, please call setTimes first"); + } + + this.weights = weights; + + assert times != null && times.length == weights.length; + } + } + + @Override + public double getLength() { + return length; + } + + @Override + public void getDataAtTime(double t, float[] store) { + float time = (float) t; + + int lastFrame = times.length - 1; + if (time < 0 || lastFrame == 0) { + if (weights != null) { + System.arraycopy(weights,0,store,0, nbMorphTargets); + } + return; + } + + int startFrame = 0; + int endFrame = 1; + float blend = 0; + if (time >= times[lastFrame]) { + startFrame = lastFrame; + + time = time - times[startFrame] + times[startFrame - 1]; + blend = (time - times[startFrame - 1]) + / (times[startFrame] - times[startFrame - 1]); + + } else { + // use lastFrame so we never overflow the array + int i; + for (i = 0; i < lastFrame && times[i] < time; i++) { + startFrame = i; + endFrame = i + 1; + } + blend = (time - times[startFrame]) + / (times[endFrame] - times[startFrame]); + } + + interpolator.interpolateWeights(blend, startFrame, weights, nbMorphTargets, store); + } + + public void setFrameInterpolator(FrameInterpolator interpolator) { + this.interpolator = interpolator; + } + + public Geometry getTarget() { + return target; + } + + public void setTarget(Geometry target) { + this.target = target; + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule oc = ex.getCapsule(this); + oc.write(weights, "weights", null); + oc.write(times, "times", null); + oc.write(target, "target", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + weights = ic.readFloatArray("weights", null); + times = ic.readFloatArray("times", null); + target = (Geometry) ic.readSavable("target", null); + setTimes(times); + } + + @Override + public Object jmeClone() { + try { + MorphTrack clone = (MorphTrack) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + this.target = cloner.clone(target); + } + + +} diff --git a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java index cc5bc317b..b5e5fb85f 100644 --- a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java +++ b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java @@ -48,7 +48,7 @@ import java.io.IOException; * * @author Rémy Bouquet */ -public class TransformTrack implements JmeCloneable, Savable { +public class TransformTrack implements AnimTrack { private double length; private HasLocalTransform target; @@ -81,15 +81,6 @@ public class TransformTrack implements JmeCloneable, Savable { this.setKeyframes(times, translations, rotations, scales); } - /** - * Creates a bone track for the given bone index - * - * @param targetJointIndex the bone's index - */ - public TransformTrack(int targetJointIndex) { - this(); - } - /** * return the array of rotations of this track * @@ -223,7 +214,7 @@ public class TransformTrack implements JmeCloneable, Savable { return length; } - public void getTransformAtTime(double t, Transform transform) { + public void getDataAtTime(double t, Transform transform) { float time = (float) t; int lastFrame = times.length - 1; diff --git a/jme3-core/src/main/java/com/jme3/anim/Weights.java b/jme3-core/src/main/java/com/jme3/anim/Weights.java new file mode 100644 index 000000000..8ac9b5b4d --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/Weights.java @@ -0,0 +1,95 @@ +package com.jme3.anim; + +import java.util.ArrayList; + +public class Weights {//} extends Savable, JmeCloneable{ + + + private final static float MIN_WEIGHT = 0.005f; + + private int[] indices; + private float[] data; + private int size; + + public Weights(float[] array, int start, int length) { + ArrayList list = new ArrayList<>(); + ArrayList idx = new ArrayList<>(); + + for (int i = start; i < length; i++) { + float val = array[i]; + if (val > MIN_WEIGHT) { + list.add(val); + idx.add(i); + } + } + size = list.size(); + data = new float[size]; + indices = new int[size]; + for (int i = 0; i < size; i++) { + data[i] = list.get(i); + indices[i] = idx.get(i); + } + } + + public int getSize() { + return size; + } + + // public Weights(float[] array, int start, int length) { +// LinkedList list = new LinkedList<>(); +// LinkedList idx = new LinkedList<>(); +// for (int i = start; i < length; i++) { +// float val = array[i]; +// if (val > MIN_WEIGHT) { +// int index = insert(list, val); +// if (idx.size() < index) { +// idx.add(i); +// } else { +// idx.add(index, i); +// } +// } +// } +// data = new float[list.size()]; +// for (int i = 0; i < data.length; i++) { +// data[i] = list.get(i); +// } +// +// indices = new int[idx.size()]; +// for (int i = 0; i < indices.length; i++) { +// indices[i] = idx.get(i); +// } +// } +// +// private int insert(LinkedList list, float value) { +// for (int i = 0; i < list.size(); i++) { +// float w = list.get(i); +// if (value > w) { +// list.add(i, value); +// return i; +// } +// } +// +// list.add(value); +// return list.size(); +// } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < indices.length; i++) { + b.append(indices[i]).append(","); + } + b.append("\n"); + for (int i = 0; i < data.length; i++) { + b.append(data[i]).append(","); + } + return b.toString(); + } + + public static void main(String... args) { + // 6 7 4 8 + float values[] = {0, 0, 0, 0, 0.5f, 0.001f, 0.7f, 0.6f, 0.2f, 0, 0, 0}; + Weights w = new Weights(values, 0, values.length); + System.err.println(w); + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolators.java b/jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolators.java index c51b73eba..5f8b5f304 100644 --- a/jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolators.java +++ b/jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolators.java @@ -58,7 +58,6 @@ public class AnimInterpolators { }; //Position / Scale interpolators - public static final AnimInterpolator LinearVec3f = new AnimInterpolator() { private Vector3f next = new Vector3f(); @@ -70,7 +69,6 @@ public class AnimInterpolators { return store; } }; - /** * CatmullRom interpolation */ diff --git a/jme3-core/src/main/java/com/jme3/anim/interpolator/FrameInterpolator.java b/jme3-core/src/main/java/com/jme3/anim/interpolator/FrameInterpolator.java index 60d40038a..9577107a8 100644 --- a/jme3-core/src/main/java/com/jme3/anim/interpolator/FrameInterpolator.java +++ b/jme3-core/src/main/java/com/jme3/anim/interpolator/FrameInterpolator.java @@ -20,6 +20,7 @@ public class FrameInterpolator { private TrackDataReader scaleReader = new TrackDataReader<>(); private TrackTimeReader timesReader = new TrackTimeReader(); + private Transform transforms = new Transform(); public Transform interpolate(float t, int currentIndex, CompactVector3Array translations, CompactQuaternionArray rotations, CompactVector3Array scales, float[] times){ @@ -42,6 +43,20 @@ public class FrameInterpolator { return transforms; } + public void interpolateWeights(float t, int currentIndex, float[] weights, int nbMorphTargets, float[] store) { + int start = currentIndex * nbMorphTargets; + for (int i = 0; i < nbMorphTargets; i++) { + int current = start + i; + int next = current + nbMorphTargets; + if (next >= weights.length) { + next = current; + } + + float val = FastMath.interpolateLinear(t, weights[current], weights[next]); + store[i] = val; + } + } + public void setTimeInterpolator(AnimInterpolator timeInterpolator) { this.timeInterpolator = timeInterpolator; } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java index 23cc9743e..ee5920828 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java @@ -1,10 +1,10 @@ package com.jme3.anim.tween.action; -import com.jme3.anim.AnimClip; -import com.jme3.anim.TransformTrack; +import com.jme3.anim.*; import com.jme3.anim.tween.AbstractTween; import com.jme3.anim.util.HasLocalTransform; import com.jme3.math.Transform; +import com.jme3.scene.Geometry; import java.util.ArrayList; import java.util.Collection; @@ -22,20 +22,41 @@ public class ClipAction extends BlendableAction { @Override public void doInterpolate(double t) { - TransformTrack[] tracks = clip.getTracks(); - for (TransformTrack track : tracks) { - HasLocalTransform target = track.getTarget(); - transform.set(target.getLocalTransform()); - track.getTransformAtTime(t, transform); - - if (collectTransformDelegate != null) { - collectTransformDelegate.collectTransform(target, transform, getWeight(), this); - } else { - this.collectTransform(target, transform, getTransitionWeight(), this); + AnimTrack[] tracks = clip.getTracks(); + for (AnimTrack track : tracks) { + if (track instanceof TransformTrack) { + interpolateTransformTrack(t, (TransformTrack) track); + } else if (track instanceof MorphTrack) { + interpolateMorphTrack(t, (MorphTrack) track); } } } + private void interpolateTransformTrack(double t, TransformTrack track) { + HasLocalTransform target = track.getTarget(); + transform.set(target.getLocalTransform()); + track.getDataAtTime(t, transform); + + if (collectTransformDelegate != null) { + collectTransformDelegate.collectTransform(target, transform, getWeight(), this); + } else { + this.collectTransform(target, transform, getTransitionWeight(), this); + } + } + private void interpolateMorphTrack(double t, MorphTrack track) { + Geometry target = track.getTarget(); + float[] weights = new float[target.getMesh().getMorphTargets().length]; + track.getDataAtTime(t, weights); + target.getMesh().setMorphState(weights); + + +// if (collectTransformDelegate != null) { +// collectTransformDelegate.collectTransform(target, transform, getWeight(), this); +// } else { +// this.collectTransform(target, transform, getTransitionWeight(), this); +// } + } + public void reset() { } @@ -43,8 +64,10 @@ public class ClipAction extends BlendableAction { @Override public Collection getTargets() { List targets = new ArrayList<>(clip.getTracks().length); - for (TransformTrack track : clip.getTracks()) { - targets.add(track.getTarget()); + for (AnimTrack track : clip.getTracks()) { + if (track instanceof TransformTrack) { + targets.add(((TransformTrack) track).getTarget()); + } } return targets; } diff --git a/jme3-core/src/main/java/com/jme3/animation/CompactArray.java b/jme3-core/src/main/java/com/jme3/animation/CompactArray.java index f251b44a2..b64e0785c 100644 --- a/jme3-core/src/main/java/com/jme3/animation/CompactArray.java +++ b/jme3-core/src/main/java/com/jme3/animation/CompactArray.java @@ -44,7 +44,7 @@ import java.util.Map; */ public abstract class CompactArray implements JmeCloneable { - private Map indexPool = new HashMap(); + protected Map indexPool = new HashMap(); protected int[] index; protected float[] array; private boolean invalid; @@ -114,6 +114,10 @@ public abstract class CompactArray implements JmeCloneable { indexPool.clear(); } + protected void setInvalid(boolean invalid) { + this.invalid = invalid; + } + /** * @param index * @param value diff --git a/jme3-core/src/main/java/com/jme3/animation/CompactFloatArray.java b/jme3-core/src/main/java/com/jme3/animation/CompactFloatArray.java new file mode 100644 index 000000000..a879ef1cb --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/animation/CompactFloatArray.java @@ -0,0 +1,100 @@ +/* + * 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.animation; + +import com.jme3.export.*; +import com.jme3.math.Vector3f; + +import java.io.IOException; + +/** + * Serialize and compress Float by indexing similar values + * @author Lim, YongHoon + */ +public class CompactFloatArray extends CompactArray implements Savable { + + /** + * Creates a compact vector array + */ + public CompactFloatArray() { + } + + /** + * creates a compact vector array + * @param dataArray the data array + * @param index the indices + */ + public CompactFloatArray(float[] dataArray, int[] index) { + super(dataArray, index); + } + + @Override + protected final int getTupleSize() { + return 1; + } + + @Override + protected final Class getElementClass() { + return Float.class; + } + + @Override + public void write(JmeExporter ex) throws IOException { + serialize(); + OutputCapsule out = ex.getCapsule(this); + out.write(array, "array", null); + out.write(index, "index", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule in = im.getCapsule(this); + array = in.readFloatArray("array", null); + index = in.readIntArray("index", null); + } + + public void fill(int startIndex, float[] store ){ + for (int i = 0; i < store.length; i++) { + store[i] = get(startIndex + i, null); + } + } + + @Override + protected void serialize(int i, Float data) { + array[i] = data; + } + + @Override + protected Float deserialize(int i, Float store) { + return array[i]; + } +} \ No newline at end of file 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 e9fccb73e..217c38f5c 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java +++ b/jme3-core/src/main/java/com/jme3/scene/Mesh.java @@ -50,6 +50,7 @@ import com.jme3.util.clone.JmeCloneable; import java.io.IOException; import java.nio.*; import java.util.ArrayList; +import java.util.Arrays; /** * Mesh is used to store rendering data. @@ -164,8 +165,8 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { private CollisionData collisionTree = null; - private SafeArrayList buffersList = new SafeArrayList(VertexBuffer.class); - private IntMap buffers = new IntMap(); + private SafeArrayList buffersList = new SafeArrayList<>(VertexBuffer.class); + private IntMap buffers = new IntMap<>(); private VertexBuffer[] lodLevels; private float pointSize = 1; private float lineWidth = 1; @@ -183,6 +184,11 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { private Mode mode = Mode.Triangles; + private SafeArrayList morphTargets; + private int numMorphBuffers = 0; + private float[] morphState; + private boolean dirtyMorph = true; + /** * Creates a new mesh with no {@link VertexBuffer vertex buffers}. */ @@ -203,7 +209,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { clone.meshBound = meshBound.clone(); clone.collisionTree = collisionTree != null ? collisionTree : null; clone.buffers = buffers.clone(); - clone.buffersList = new SafeArrayList(VertexBuffer.class,buffersList); + clone.buffersList = new SafeArrayList<>(VertexBuffer.class, buffersList); clone.vertexArrayID = -1; if (elementLengths != null) { clone.elementLengths = elementLengths.clone(); @@ -233,8 +239,8 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { //clone.collisionTree = collisionTree != null ? collisionTree : null; clone.collisionTree = null; // it will get re-generated in any case - clone.buffers = new IntMap(); - clone.buffersList = new SafeArrayList(VertexBuffer.class); + clone.buffers = new IntMap<>(); + clone.buffersList = new SafeArrayList<>(VertexBuffer.class); for (VertexBuffer vb : buffersList.getArray()){ VertexBuffer bufClone = vb.clone(); clone.buffers.put(vb.getBufferType().ordinal(), bufClone); @@ -697,7 +703,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { */ @Deprecated public void setInterleaved(){ - ArrayList vbs = new ArrayList(); + ArrayList vbs = new ArrayList<>(); vbs.addAll(buffersList); // ArrayList vbs = new ArrayList(buffers.values()); @@ -820,8 +826,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { * {@link #setInterleaved() interleaved} format. */ public void updateCounts(){ - if (getBuffer(Type.InterleavedData) != null) + if (getBuffer(Type.InterleavedData) != null) { throw new IllegalStateException("Should update counts before interleave"); + } VertexBuffer pb = getBuffer(Type.Position); VertexBuffer ib = getBuffer(Type.Index); @@ -844,11 +851,13 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { */ public int getTriangleCount(int lod){ if (lodLevels != null){ - if (lod < 0) + if (lod < 0) { throw new IllegalArgumentException("LOD level cannot be < 0"); + } - if (lod >= lodLevels.length) - throw new IllegalArgumentException("LOD level "+lod+" does not exist!"); + if (lod >= lodLevels.length) { + throw new IllegalArgumentException("LOD level " + lod + " does not exist!"); + } return computeNumElements(lodLevels[lod].getData().limit()); }else if (lod == 0){ @@ -968,8 +977,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { * Sets the mesh's VAO ID. Internal use only. */ public void setId(int id){ - if (vertexArrayID != -1) + if (vertexArrayID != -1) { throw new IllegalStateException("ID has already been set."); + } vertexArrayID = id; } @@ -1037,8 +1047,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { * @throws IllegalArgumentException If the buffer type is already set */ public void setBuffer(VertexBuffer vb){ - if (buffers.containsKey(vb.getBufferType().ordinal())) - throw new IllegalArgumentException("Buffer type already set: "+vb.getBufferType()); + if (buffers.containsKey(vb.getBufferType().ordinal())) { + throw new IllegalArgumentException("Buffer type already set: " + vb.getBufferType()); + } buffers.put(vb.getBufferType().ordinal(), vb); buffersList.add(vb); @@ -1151,8 +1162,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { */ public FloatBuffer getFloatBuffer(Type type) { VertexBuffer vb = getBuffer(type); - if (vb == null) + if (vb == null) { return null; + } return (FloatBuffer) vb.getData(); } @@ -1166,8 +1178,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { */ public ShortBuffer getShortBuffer(Type type) { VertexBuffer vb = getBuffer(type); - if (vb == null) + if (vb == null) { return null; + } return (ShortBuffer) vb.getData(); } @@ -1179,8 +1192,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { * @return A virtual or wrapped index buffer to read the data as a list */ public IndexBuffer getIndicesAsList(){ - if (mode == Mode.Hybrid) + if (mode == Mode.Hybrid) { throw new UnsupportedOperationException("Hybrid mode not supported"); + } IndexBuffer ib = getIndexBuffer(); if (ib != null){ @@ -1209,8 +1223,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { */ public IndexBuffer getIndexBuffer() { VertexBuffer vb = getBuffer(Type.Index); - if (vb == null) + if (vb == null) { return null; + } return IndexBuffer.wrapIndexBuffer(vb.getData()); } @@ -1233,8 +1248,8 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { IndexBuffer indexBuf = getIndexBuffer(); int numIndices = indexBuf.size(); - IntMap oldIndicesToNewIndices = new IntMap(numIndices); - ArrayList newIndicesToOldIndices = new ArrayList(); + IntMap oldIndicesToNewIndices = new IntMap<>(numIndices); + ArrayList newIndicesToOldIndices = new ArrayList<>(); int newIndex = 0; for (int i = 0; i < numIndices; i++) { @@ -1345,14 +1360,17 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { */ public void scaleTextureCoordinates(Vector2f scaleFactor){ VertexBuffer tc = getBuffer(Type.TexCoord); - if (tc == null) + if (tc == null) { throw new IllegalStateException("The mesh has no texture coordinates"); + } - if (tc.getFormat() != VertexBuffer.Format.Float) + if (tc.getFormat() != VertexBuffer.Format.Float) { throw new UnsupportedOperationException("Only float texture coord format is supported"); + } - if (tc.getNumComponents() != 2) + if (tc.getNumComponents() != 2) { throw new UnsupportedOperationException("Only 2D texture coords are supported"); + } FloatBuffer fb = (FloatBuffer) tc.getData(); fb.clear(); @@ -1504,6 +1522,70 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { return patchVertexCount; } + + public void addMorphTarget(MorphTarget target) { + if (morphTargets == null) { + morphTargets = new SafeArrayList<>(MorphTarget.class); + } +// if (numMorphBuffers == 0) { +// numMorphBuffers = target.getNumBuffers(); +// int start = Type.MorphTarget0.ordinal(); +// int end = start + numMorphBuffers; +// for (int i = start; i < end; i++) { +// VertexBuffer vb = new VertexBuffer(Type.values()[i]); +// setBuffer(vb); +// } +// } else if (target.getNumBuffers() != numMorphBuffers) { +// throw new IllegalArgumentException("Morph target has different number of buffers"); +// } + + morphTargets.add(target); + } + + public void setMorphState(float[] state) { + if (morphTargets.isEmpty()) { + return; + } + if (morphState == null) { + morphState = new float[morphTargets.size()]; + } + System.arraycopy(state, 0, morphState, 0, morphState.length); + this.dirtyMorph = true; + } + + public float[] getMorphState() { + if (morphState == null) { + morphState = new float[morphTargets.size()]; + } + return morphState; + } + + public void setActiveMorphTargets(int... targetsIndex) { + int start = Type.MorphTarget0.ordinal(); + for (int i = 0; i < targetsIndex.length; i++) { + MorphTarget t = morphTargets.get(targetsIndex[i]); + int idx = 0; + for (Type type : t.getBuffers().keySet()) { + FloatBuffer b = t.getBuffer(type); + setBuffer(Type.values()[start + i + idx], 3, b); + idx++; + } + } + } + + public MorphTarget[] getMorphTargets() { + return morphTargets.getArray(); + } + + public boolean hasMorphTargets() { + return morphTargets != null && !morphTargets.isEmpty(); + } + + public boolean isDirtyMorph() { + return dirtyMorph; + } + + @Override public void write(JmeExporter ex) throws IOException { OutputCapsule out = ex.getCapsule(this); @@ -1550,6 +1632,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { out.write(lodLevels, "lodLevels", null); } + @Override public void read(JmeImporter im) throws IOException { InputCapsule in = im.getCapsule(this); meshBound = (BoundingVolume) in.readSavable("modelBound", null); diff --git a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java index 001cf570d..9753850a1 100644 --- a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java +++ b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java @@ -212,7 +212,41 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { * Format should be {@link Format#Float} and number of components * should be 16. */ - InstanceData + InstanceData, + + /** + * Morph animations targets. + * Supports up tp 8 morph target buffers at the same time + * Limited due to the limited number of attributes you can bind to a vertex shader usually 16 + *

+ * MorphTarget buffers are either POSITION, NORMAL or TANGENT buffers. + * So we can support up to + * 10 simultaneous POSITION targets + * 5 simultaneous POSITION and NORMAL targets + * 3 simultaneous POSTION, NORMAL and TANGENT targets. + *

+ * Note that all buffers have 3 components (Vector3f) even the Tangent buffer that + * does not contain the w (handedness) component that will not be interpolated for morph animation. + *

+ * Note that those buffers contain the difference between the base buffer (POSITION, NORMAL or TANGENT) and the target value + * So that you can interpolate with a MADD operation in the vertex shader + * position = weight * diffPosition + basePosition; + */ + MorphTarget0, + MorphTarget1, + MorphTarget2, + MorphTarget3, + MorphTarget4, + MorphTarget5, + MorphTarget6, + MorphTarget7, + MorphTarget8, + MorphTarget9, + MorphTarget10, + MorphTarget11, + MorphTarget12, + MorphTarget13, + } /** @@ -241,7 +275,7 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { * Mesh data is not sent to GPU at all. It is only * used by the CPU. */ - CpuOnly; + CpuOnly } /** @@ -610,8 +644,9 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { return 0; } int elements = data.limit() / components; - if (format == Format.Half) + if (format == Format.Half) { elements /= 2; + } return elements; } @@ -642,14 +677,17 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { * argument. */ public void setupData(Usage usage, int components, Format format, Buffer data){ - if (id != -1) + if (id != -1) { throw new UnsupportedOperationException("Data has already been sent. Cannot setupData again."); + } - if (usage == null || format == null || data == null) + if (usage == null || format == null || data == null) { throw new IllegalArgumentException("None of the arguments can be null"); - - if (data.isReadOnly()) - throw new IllegalArgumentException( "VertexBuffer data cannot be read-only." ); + } + + if (data.isReadOnly()) { + throw new IllegalArgumentException("VertexBuffer data cannot be read-only."); + } if (bufType != Type.InstanceData) { if (components < 1 || components > 4) { @@ -720,11 +758,13 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { * Converts single floating-point data to {@link Format#Half half} floating-point data. */ public void convertToHalf(){ - if (id != -1) + if (id != -1) { throw new UnsupportedOperationException("Data has already been sent."); + } - if (format != Format.Float) + if (format != Format.Float) { throw new IllegalStateException("Format must be float!"); + } int numElements = data.limit() / components; format = Format.Half; @@ -913,8 +953,9 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { * match. */ public void copyElements(int inIndex, VertexBuffer outVb, int outIndex, int len){ - if (outVb.format != format || outVb.components != components) + if (outVb.format != format || outVb.components != components) { throw new IllegalArgumentException("Buffer format mismatch. Cannot copy"); + } int inPos = inIndex * components; int outPos = outIndex * components; @@ -981,8 +1022,9 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { * of elements with the given number of components in each element. */ public static Buffer createBuffer(Format format, int components, int numElements){ - if (components < 1 || components > 4) + if (components < 1 || components > 4) { throw new IllegalArgumentException("Num components must be between 1 and 4"); + } int total = numElements * components; diff --git a/jme3-core/src/main/java/com/jme3/scene/mesh/MorphTarget.java b/jme3-core/src/main/java/com/jme3/scene/mesh/MorphTarget.java new file mode 100644 index 000000000..8f65473a2 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/mesh/MorphTarget.java @@ -0,0 +1,40 @@ +package com.jme3.scene.mesh; + +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.Savable; +import com.jme3.scene.VertexBuffer; + +import java.io.IOException; +import java.nio.FloatBuffer; +import java.util.EnumMap; + +public class MorphTarget implements Savable { + private EnumMap buffers = new EnumMap<>(VertexBuffer.Type.class); + + public void setBuffer(VertexBuffer.Type type, FloatBuffer buffer) { + buffers.put(type, buffer); + } + + public FloatBuffer getBuffer(VertexBuffer.Type type) { + return buffers.get(type); + } + + public EnumMap getBuffers() { + return buffers; + } + + public int getNumBuffers() { + return buffers.size(); + } + + @Override + public void write(JmeExporter ex) throws IOException { + + } + + @Override + public void read(JmeImporter im) throws IOException { + + } +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md b/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md index bc699e6b1..2371b79a7 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md @@ -110,6 +110,11 @@ MaterialDef Phong Lighting { // For hardware skinning Int NumberOfBones Matrix4Array BoneMatrices + + // For Morph animation + FloatArray MorphWeights + Int NumberOfMorphTargets + Int NumberOfTargetsBuffers : 1 //For instancing Boolean UseInstancing @@ -152,6 +157,8 @@ MaterialDef Phong Lighting { SPHERE_MAP : EnvMapAsSphereMap NUM_BONES : NumberOfBones INSTANCING : UseInstancing + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } } @@ -191,6 +198,8 @@ MaterialDef Phong Lighting { SPHERE_MAP : EnvMapAsSphereMap NUM_BONES : NumberOfBones INSTANCING : UseInstancing + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } } @@ -210,6 +219,8 @@ MaterialDef Phong Lighting { DISCARD_ALPHA : AlphaDiscardThreshold NUM_BONES : NumberOfBones INSTANCING : UseInstancing + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } ForcedRenderState { @@ -247,6 +258,8 @@ MaterialDef Phong Lighting { NUM_BONES : NumberOfBones INSTANCING : UseInstancing BACKFACE_SHADOWS: BackfaceShadows + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } ForcedRenderState { @@ -274,6 +287,8 @@ MaterialDef Phong Lighting { DIFFUSEMAP_ALPHA : DiffuseMap NUM_BONES : NumberOfBones INSTANCING : UseInstancing + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } } @@ -296,6 +311,8 @@ MaterialDef Phong Lighting { NUM_BONES : NumberOfBones INSTANCING : UseInstancing + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } } diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.vert b/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.vert index 395bc1bca..6c74a7557 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.vert @@ -2,6 +2,8 @@ #import "Common/ShaderLib/Instancing.glsllib" #import "Common/ShaderLib/Skinning.glsllib" #import "Common/ShaderLib/Lighting.glsllib" +#import "Common/ShaderLib/MorphAnim.glsllib" + #ifdef VERTEX_LIGHTING #import "Common/ShaderLib/BlinnPhongLighting.glsllib" #endif @@ -90,6 +92,14 @@ void main(){ vec3 modelSpaceTan = inTangent.xyz; #endif + #ifdef NUM_MORPH_TARGETS + #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING) + Morph_Compute(modelSpacePos, modelSpaceNorm, modelSpaceTan); + #else + Morph_Compute(modelSpacePos, modelSpaceNorm); + #endif + #endif + #ifdef NUM_BONES #ifndef VERTEX_LIGHTING Skinning_Compute(modelSpacePos, modelSpaceNorm, modelSpaceTan); diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md index afca96d4c..2a0849439 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md @@ -110,6 +110,11 @@ MaterialDef PBR Lighting { // For hardware skinning Int NumberOfBones Matrix4Array BoneMatrices + + // For Morph animation + FloatArray MorphWeights + Int NumberOfMorphTargets + Int NumberOfTargetsBuffers : 1 //For instancing Boolean UseInstancing @@ -158,6 +163,8 @@ MaterialDef PBR Lighting { NORMAL_TYPE: NormalType VERTEX_COLOR : UseVertexColor AO_MAP: LightMapAsAOMap + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } } @@ -178,6 +185,8 @@ MaterialDef PBR Lighting { DISCARD_ALPHA : AlphaDiscardThreshold NUM_BONES : NumberOfBones INSTANCING : UseInstancing + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } ForcedRenderState { @@ -215,6 +224,8 @@ MaterialDef PBR Lighting { NUM_BONES : NumberOfBones INSTANCING : UseInstancing BACKFACE_SHADOWS: BackfaceShadows + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } ForcedRenderState { @@ -247,6 +258,8 @@ MaterialDef PBR Lighting { NUM_BONES : NumberOfBones INSTANCING : UseInstancing BACKFACE_SHADOWS: BackfaceShadows + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } ForcedRenderState { @@ -272,6 +285,8 @@ MaterialDef PBR Lighting { Defines { NUM_BONES : NumberOfBones INSTANCING : UseInstancing + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } } @@ -291,6 +306,8 @@ MaterialDef PBR Lighting { NEED_TEXCOORD1 NUM_BONES : NumberOfBones INSTANCING : UseInstancing + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } } diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.vert b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.vert index 77782456b..b910a8d4b 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.vert @@ -1,10 +1,9 @@ #import "Common/ShaderLib/GLSLCompat.glsllib" #import "Common/ShaderLib/Instancing.glsllib" #import "Common/ShaderLib/Skinning.glsllib" - +#import "Common/ShaderLib/MorphAnim.glsllib" uniform vec4 m_BaseColor; - uniform vec4 g_AmbientLightColor; varying vec2 texCoord; @@ -38,11 +37,19 @@ void main(){ vec3 modelSpaceTan = inTangent.xyz; #endif + #ifdef NUM_MORPH_TARGETS + #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING) + Morph_Compute(modelSpacePos, modelSpaceNorm, modelSpaceTan); + #else + Morph_Compute(modelSpacePos, modelSpaceNorm); + #endif + #endif + #ifdef NUM_BONES #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING) - Skinning_Compute(modelSpacePos, modelSpaceNorm, modelSpaceTan); + Skinning_Compute(modelSpacePos, modelSpaceNorm, modelSpaceTan); #else - Skinning_Compute(modelSpacePos, modelSpaceNorm); + Skinning_Compute(modelSpacePos, modelSpaceNorm); #endif #endif @@ -64,4 +71,4 @@ void main(){ #ifdef VERTEX_COLOR Color *= inColor; #endif -} \ No newline at end of file +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.vert b/jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.vert index 49cc5b96e..f9fbe40cf 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.vert @@ -2,6 +2,8 @@ #import "Common/ShaderLib/Instancing.glsllib" #import "Common/ShaderLib/Skinning.glsllib" #import "Common/ShaderLib/Lighting.glsllib" +#import "Common/ShaderLib/MorphAnim.glsllib" + #ifdef VERTEX_LIGHTING #import "Common/ShaderLib/BlinnPhongLighting.glsllib" #endif @@ -84,6 +86,14 @@ void main(){ vec3 modelSpaceTan = inTangent.xyz; #endif + #ifdef NUM_MORPH_TARGETS + #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING) + Morph_Compute(modelSpacePos, modelSpaceNorm, modelSpaceTan); + #else + Morph_Compute(modelSpacePos, modelSpaceNorm); + #endif + #endif + #ifdef NUM_BONES #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING) Skinning_Compute(modelSpacePos, modelSpaceNorm, modelSpaceTan); diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md index 826a44e45..b3e14ee9a 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md @@ -20,6 +20,11 @@ MaterialDef Unshaded { Int NumberOfBones Matrix4Array BoneMatrices + // For Morph animation + FloatArray MorphWeights + Int NumberOfMorphTargets + Int NumberOfTargetsBuffers : 1 + // Alpha threshold for fragment discarding Float AlphaDiscardThreshold (AlphaTestFallOff) @@ -76,26 +81,30 @@ MaterialDef Unshaded { HAS_COLOR : Color NUM_BONES : NumberOfBones DISCARD_ALPHA : AlphaDiscardThreshold + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } } Technique PreNormalPass { - VertexShader GLSL100 GLSL150 : Common/MatDefs/SSAO/normal.vert - FragmentShader GLSL100 GLSL150 : Common/MatDefs/SSAO/normal.frag - - WorldParameters { - WorldViewProjectionMatrix - WorldViewMatrix - NormalMatrix - ViewProjectionMatrix - ViewMatrix - } - - Defines { - NUM_BONES : NumberOfBones - INSTANCING : UseInstancing - } + VertexShader GLSL100 GLSL150 : Common/MatDefs/SSAO/normal.vert + FragmentShader GLSL100 GLSL150 : Common/MatDefs/SSAO/normal.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldViewMatrix + NormalMatrix + ViewProjectionMatrix + ViewMatrix + } + + Defines { + NUM_BONES : NumberOfBones + INSTANCING : UseInstancing + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers + } } Technique PreShadow { @@ -115,6 +124,8 @@ MaterialDef Unshaded { DISCARD_ALPHA : AlphaDiscardThreshold NUM_BONES : NumberOfBones INSTANCING : UseInstancing + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } ForcedRenderState { @@ -150,8 +161,10 @@ MaterialDef Unshaded { PSSM : Splits POINTLIGHT : LightViewProjectionMatrix5 NUM_BONES : NumberOfBones - INSTANCING : UseInstancing + INSTANCING : UseInstancing BACKFACE_SHADOWS: BackfaceShadows + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } ForcedRenderState { @@ -177,8 +190,10 @@ MaterialDef Unshaded { HAS_GLOWMAP : GlowMap HAS_GLOWCOLOR : GlowColor NUM_BONES : NumberOfBones - INSTANCING : UseInstancing + INSTANCING : UseInstancing HAS_POINTSIZE : PointSize + NUM_MORPH_TARGETS: NumberOfMorphTargets + NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers } } } \ No newline at end of file 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 6cf9d9484..2c694d45d 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.vert @@ -1,6 +1,7 @@ #import "Common/ShaderLib/GLSLCompat.glsllib" #import "Common/ShaderLib/Skinning.glsllib" #import "Common/ShaderLib/Instancing.glsllib" +#import "Common/ShaderLib/MorphAnim.glsllib" attribute vec3 inPosition; @@ -38,6 +39,11 @@ void main(){ #endif vec4 modelSpacePos = vec4(inPosition, 1.0); + + #ifdef NUM_MORPH_TARGETS + Morph_Compute(modelSpacePos); + #endif + #ifdef NUM_BONES Skinning_Compute(modelSpacePos); #endif diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale100.frag b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale100.frag index b15415b5f..98a05d116 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale100.frag +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/FixedScale100.frag @@ -1,5 +1,5 @@ void main(){ - vec4 worldPos = worldMatrix * vec4(modelPosition, 1.0); + vec4 worldPos = worldMatrix * vec4(0.0, 0.0, 0.0, 1.0); vec3 dir = worldPos.xyz - cameraPos; float distance = dot(cameraDir, dir); float m11 = projectionMatrix[1][1]; diff --git a/jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.vert b/jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.vert index 417427155..15be15f8d 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.vert @@ -49,10 +49,13 @@ const mat4 biasMat = mat4(0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 1.0); - void main(){ vec4 modelSpacePos = vec4(inPosition, 1.0); - + + #ifdef NUM_MORPH_TARGETS + Morph_Compute(modelSpacePos); + #endif + #ifdef NUM_BONES Skinning_Compute(modelSpacePos); #endif diff --git a/jme3-core/src/main/resources/Common/MatDefs/Shadow/PreShadow.vert b/jme3-core/src/main/resources/Common/MatDefs/Shadow/PreShadow.vert index 4e3023d7f..7157eea30 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Shadow/PreShadow.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Shadow/PreShadow.vert @@ -1,6 +1,8 @@ #import "Common/ShaderLib/GLSLCompat.glsllib" #import "Common/ShaderLib/Instancing.glsllib" #import "Common/ShaderLib/Skinning.glsllib" +#import "Common/ShaderLib/MorphAnim.glsllib" + attribute vec3 inPosition; attribute vec2 inTexCoord; @@ -8,7 +10,11 @@ varying vec2 texCoord; void main(){ vec4 modelSpacePos = vec4(inPosition, 1.0); - + + #ifdef NUM_MORPH_TARGETS + Morph_Compute(modelSpacePos, modelSpaceNorm); + #endif + #ifdef NUM_BONES Skinning_Compute(modelSpacePos); #endif diff --git a/jme3-core/src/main/resources/Common/ShaderLib/MorphAnim.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/MorphAnim.glsllib new file mode 100644 index 000000000..68c26f4ae --- /dev/null +++ b/jme3-core/src/main/resources/Common/ShaderLib/MorphAnim.glsllib @@ -0,0 +1,212 @@ +/** +A glsllib that perform morph animation. +Note that it only handles morphing position, normals and tangents. +*/ +#ifdef NUM_MORPH_TARGETS + vec3 dummy_norm = vec3(0.0); + vec3 dummy_tan = vec3(0.0); + #define NUM_BUFFERS NUM_MORPH_TARGETS * NUM_TARGETS_BUFFERS + #if (NUM_BUFFERS > 0) + uniform float m_MorphWeights[NUM_MORPH_TARGETS]; + attribute vec3 inMorphTarget0; + #endif + #if (NUM_BUFFERS > 1) + attribute vec3 inMorphTarget1; + #endif + #if (NUM_BUFFERS > 2) + attribute vec3 inMorphTarget2; + #endif + #if (NUM_BUFFERS > 3) + attribute vec3 inMorphTarget3; + #endif + #if (NUM_BUFFERS > 4) + attribute vec3 inMorphTarget4; + #endif + #if (NUM_BUFFERS > 5) + attribute vec3 inMorphTarget5; + #endif + #if (NUM_BUFFERS > 6) + attribute vec3 inMorphTarget6; + #endif + #if (NUM_BUFFERS > 7) + attribute vec3 inMorphTarget7; + #endif + #if (NUM_BUFFERS > 8) + attribute vec3 inMorphTarget8; + #endif + #if (NUM_BUFFERS > 9) + attribute vec3 inMorphTarget9; + #endif + #if (NUM_BUFFERS > 10) + attribute vec3 inMorphTarget10; + #endif + #if (NUM_BUFFERS > 11) + attribute vec3 inMorphTarget11; + #endif + #if (NUM_BUFFERS > 12) + attribute vec3 inMorphTarget12; + #endif + #if (NUM_BUFFERS > 13) + attribute vec3 inMorphTarget13; + #endif + + void Morph_Compute_Pos(inout vec4 pos){ + #if (NUM_TARGETS_BUFFERS == 1) + #if (NUM_MORPH_TARGETS > 0) + pos.xyz += m_MorphWeights[0] * inMorphTarget0; + #endif + #if (NUM_MORPH_TARGETS > 1) + pos.xyz += m_MorphWeights[1] * inMorphTarget1; + #endif + #if (NUM_MORPH_TARGETS > 2) + pos.xyz += m_MorphWeights[2] * inMorphTarget2; + #endif + #if (NUM_MORPH_TARGETS > 3) + pos.xyz += m_MorphWeights[3] * inMorphTarget3; + #endif + #if (NUM_MORPH_TARGETS > 4) + pos.xyz += m_MorphWeights[4] * inMorphTarget4; + #endif + #if (NUM_MORPH_TARGETS > 5) + pos.xyz += m_MorphWeights[5] * inMorphTarget5; + #endif + #if (NUM_MORPH_TARGETS > 6) + pos.xyz += m_MorphWeights[6] * inMorphTarget6; + #endif + #if (NUM_MORPH_TARGETS > 7) + pos.xyz += m_MorphWeights[7] * inMorphTarget7; + #endif + #if (NUM_MORPH_TARGETS > 8) + pos.xyz += m_MorphWeights[8] * inMorphTarget8; + #endif + #if (NUM_MORPH_TARGETS > 9) + pos.xyz += m_MorphWeights[9] * inMorphTarget9; + #endif + #if (NUM_MORPH_TARGETS > 10) + pos.xyz += m_MorphWeights[10] * inMorphTarget10; + #endif + #if (NUM_MORPH_TARGETS > 11) + pos.xyz += m_MorphWeights[11] * inMorphTarget11; + #endif + #if (NUM_MORPH_TARGETS > 12) + pos.xyz += m_MorphWeights[12] * inMorphTarget12; + #endif + #if (NUM_MORPH_TARGETS > 13) + pos.xyz += m_MorphWeights[13] * inMorphTarget13; + #endif + #endif + } + + float Get_Inverse_Weights_Sum(){ + float sum = 0; + for( int i = 0;i < NUM_MORPH_TARGETS; i++){ + sum += m_MorphWeights[i]; + } + return 1.0 / max(1.0, sum); + } + + void Morph_Compute_Pos_Norm(inout vec4 pos, inout vec3 norm){ + #if (NUM_TARGETS_BUFFERS == 2) + // weight sum may be over 1.0. It's totallyvalid for position + // but for normals. the weights needs to be normalized. + float invWeightsSum = Get_Inverse_Weights_Sum(); + #if (NUM_BUFFERS > 1) + pos.xyz += m_MorphWeights[0] * inMorphTarget0; + norm += m_MorphWeights[0] * invWeightsSum * inMorphTarget1; + #endif + #if (NUM_BUFFERS > 3) + pos.xyz += m_MorphWeights[1] * inMorphTarget2; + norm.xyz += m_MorphWeights[1] * invWeightsSum * inMorphTarget3; + #endif + #if (NUM_BUFFERS > 5) + pos.xyz += m_MorphWeights[2] * inMorphTarget4; + norm += m_MorphWeights[2] * invWeightsSum * inMorphTarget5; + #endif + #if (NUM_BUFFERS > 7) + pos.xyz += m_MorphWeights[3] * inMorphTarget6; + norm += m_MorphWeights[3] * invWeightsSum * inMorphTarget7; + #endif + #if (NUM_BUFFERS > 9) + pos.xyz += m_MorphWeights[4] * inMorphTarget8; + norm += m_MorphWeights[4] * invWeightsSum * inMorphTarget9; + #endif + #if (NUM_BUFFERS > 11) + pos.xyz += m_MorphWeights[5] * inMorphTarget10; + norm += m_MorphWeights[5] * invWeightsSum * inMorphTarget11; + #endif + #if (NUM_BUFFERS > 13) + pos.xyz += m_MorphWeights[6] * inMorphTarget12; + norm += m_MorphWeights[6] * invWeightsSum * inMorphTarget13; + #endif + #endif + } + + void Morph_Compute_Pos_Norm_Tan(inout vec4 pos, inout vec3 norm, inout vec3 tan){ + #if (NUM_TARGETS_BUFFERS == 3) + // weight sum may be over 1.0. It's totallyvalid for position + // but for normals. the weights needs to be normalized. + float invWeightsSum = Get_Inverse_Weights_Sum(); + #if (NUM_BUFFERS > 2) + float normWeight = m_MorphWeights[0] * invWeightsSum; + pos.xyz += m_MorphWeights[0] * inMorphTarget0; + norm += normWeight * inMorphTarget1; + tan += normWeight * inMorphTarget2; + #endif + #if (NUM_BUFFERS > 5) + float normWeight = m_MorphWeights[1] * invWeightsSum; + pos.xyz += m_MorphWeights[1] * inMorphTarget3; + norm += normWeight * inMorphTarget4; + tan += normWeight * inMorphTarget5; + #endif + #if (NUM_BUFFERS > 8) + float normWeight = m_MorphWeights[2] * invWeightsSum; + pos.xyz += m_MorphWeights[2] * inMorphTarget6; + norm += normWeight * inMorphTarget7; + tan += normWeight * inMorphTarget8; + #endif + #if (NUM_BUFFERS > 11) + float normWeight = m_MorphWeights[3] * invWeightsSum; + pos.xyz += m_MorphWeights[3] * inMorphTarget9; + norm += normWeight * inMorphTarget10; + tan += normWeight * inMorphTarget11; + #endif + #endif + } + + void Morph_Compute(inout vec4 pos){ + #if (NUM_TARGETS_BUFFERS == 2) + Morph_Compute_Pos_Norm(pos,dummy_norm); + return; + #elif (NUM_TARGETS_BUFFERS == 3) + Morph_Compute_Pos_Norm_Tan(pos, dummy_norm, dummy_tan); + return; + #endif + Morph_Compute_Pos(pos); + } + + void Morph_Compute(inout vec4 pos, inout vec3 norm){ + #if (NUM_TARGETS_BUFFERS == 1) + Morph_Compute_Pos(pos); + return; + #elif (NUM_TARGETS_BUFFERS == 3) + Morph_Compute_Pos_Norm_Tan(pos, dummy_norm, dummy_tan); + return; + #elif (NUM_TARGETS_BUFFERS == 2) + Morph_Compute_Pos_Norm(pos, norm); + #endif + } + + void Morph_Compute(inout vec4 pos, inout vec3 norm, inout vec3 tan){ + #if (NUM_TARGETS_BUFFERS == 1) + Morph_Compute_Pos(pos); + return; + #elif (NUM_TARGETS_BUFFERS == 2) + Morph_Compute_Pos_Norm(pos, norm); + tan = normalize(tan - dot(tan, norm) * norm); + return; + #elif (NUM_TARGETS_BUFFERS == 3) + Morph_Compute_Pos_Norm_Tan(pos, norm, tan); + #endif + } + +#endif diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index f9f818f3c..be2908194 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -41,11 +41,12 @@ import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.math.*; import com.jme3.renderer.Limits; -import com.jme3.scene.Node; -import com.jme3.scene.Spatial; +import com.jme3.scene.*; import com.jme3.scene.control.Control; import com.jme3.scene.debug.custom.ArmatureDebugAppState; import com.jme3.scene.plugins.gltf.GltfModelKey; +import com.jme3.shader.VarType; +import jme3test.model.anim.EraseTimer; import java.util.*; @@ -58,9 +59,13 @@ public class TestGltfLoading extends SimpleApplication { int assetIndex = 0; boolean useAutoRotate = false; private final static String indentString = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"; - int duration = 2; + int duration = 1; boolean playAnim = true; + Geometry g; + int morphIndex = 0; + + public static void main(String[] args) { TestGltfLoading app = new TestGltfLoading(); app.start(); @@ -73,10 +78,12 @@ public class TestGltfLoading extends SimpleApplication { https://sketchfab.com/features/gltf You have to copy them in Model/gltf folder in the test-data project. */ + @Override public void simpleInitApp() { ArmatureDebugAppState armatureDebugappState = new ArmatureDebugAppState(); getStateManager().attach(armatureDebugappState); + setTimer(new EraseTimer()); String folder = System.getProperty("user.home"); assetManager.registerLocator(folder, FileLocator.class); @@ -110,7 +117,14 @@ public class TestGltfLoading extends SimpleApplication { // PointLight pl1 = new PointLight(new Vector3f(-5.0f, -5.0f, -5.0f), ColorRGBA.White.mult(0.5f), 50); // rootNode.addLight(pl1); - loadModel("Models/gltf/polly/project_polly.gltf", new Vector3f(0, 0, 0), 0.5f); + //loadModel("Models/gltf/polly/project_polly.gltf", new Vector3f(0, 0, 0), 0.5f); + //loadModel("Models/gltf/zophrac/scene.gltf", new Vector3f(0, 0, 0), 0.1f); + loadModel("Models/gltf/scifigirl/scene.gltf", new Vector3f(0, -1, 0), 0.1f); + //loadModel("Models/gltf/man/scene.gltf", new Vector3f(0, -1, 0), 0.1f); + //loadModel("Models/gltf/torus/scene.gltf", new Vector3f(0, -1, 0), 0.1f); + //loadModel("Models/gltf/morph/scene.gltf", new Vector3f(0, 0, 0), 0.2f); + //loadModel("Models/gltf/morphCube/AnimatedMorphCube.gltf", new Vector3f(0, 0, 0), 1f); + // loadModel("Models/gltf/morph/SimpleMorph.gltf", new Vector3f(0, 0, 0), 0.1f); //loadModel("Models/gltf/nier/scene.gltf", new Vector3f(0, -1.5f, 0), 0.01f); //loadModel("Models/gltf/izzy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/darth/scene.gltf", new Vector3f(0, -1, 0), 0.01f); @@ -149,6 +163,8 @@ public class TestGltfLoading extends SimpleApplication { probeNode.attachChild(assets.get(0)); + // setMorphTarget(morphIndex); + ChaseCameraAppState chaseCam = new ChaseCameraAppState(); chaseCam.setTarget(probeNode); getStateManager().attach(chaseCam); @@ -200,6 +216,15 @@ public class TestGltfLoading extends SimpleApplication { dumpScene(rootNode, 0); } + public void setMorphTarget(int index) { + g = (Geometry) probeNode.getChild("0"); + g.getMesh().setActiveMorphTargets(index); + g.getMaterial().setInt("NumberOfMorphTargets", 1); + g.getMaterial().setInt("NumberOfTargetsBuffers", 3); + float[] weights = {1.0f}; + g.getMaterial().setParam("MorphWeights", VarType.FloatArray, weights); + } + private T findControl(Spatial s, Class controlClass) { T ctrl = s.getControl(controlClass); if (ctrl != null) { @@ -291,18 +316,19 @@ public class TestGltfLoading extends SimpleApplication { @Override public void simpleUpdate(float tpf) { - if (!useAutoRotate) { return; } time += tpf; - autoRotate.rotate(0, tpf * 0.5f, 0); + // autoRotate.rotate(0, tpf * 0.5f, 0); if (time > duration) { + // morphIndex++; + // setMorphTarget(morphIndex); assets.get(assetIndex).removeFromParent(); assetIndex = (assetIndex + 1) % assets.size(); - if (assetIndex == 0) { - duration = 10; - } +// if (assetIndex == 0) { +// duration = 10; +// } probeNode.attachChild(assets.get(assetIndex)); time = 0; } diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java index 9862ebffa..475075267 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java @@ -52,9 +52,9 @@ public class TestHWSkinning extends SimpleApplication implements ActionListener{ // private AnimComposer composer; private String[] animNames = {"Dodge", "Walk", "pull", "push"}; - private final static int SIZE = 60; + private final static int SIZE = 40; private boolean hwSkinningEnable = true; - private List skControls = new ArrayList(); + private List skControls = new ArrayList<>(); private BitmapText hwsText; public static void main(String[] args) { diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestMorph.java b/jme3-examples/src/main/java/jme3test/model/anim/TestMorph.java new file mode 100644 index 000000000..d02ff40c1 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestMorph.java @@ -0,0 +1,121 @@ +package jme3test.model.anim; + +import com.jme3.app.ChaseCameraAppState; +import com.jme3.app.SimpleApplication; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.AnalogListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.mesh.MorphTarget; +import com.jme3.scene.shape.Box; +import com.jme3.shader.VarType; +import com.jme3.util.BufferUtils; + +import java.nio.FloatBuffer; + +public class TestMorph extends SimpleApplication { + + float[] weights = new float[2]; + + public static void main(String... args) { + TestMorph app = new TestMorph(); + app.start(); + } + + @Override + public void simpleInitApp() { + final Box box = new Box(1, 1, 1); + FloatBuffer buffer = BufferUtils.createVector3Buffer(box.getVertexCount()); + + float[] d = new float[box.getVertexCount() * 3]; + for (int i = 0; i < d.length; i++) { + d[i] = 0; + } + + d[12] = 1f; + d[15] = 1f; + d[18] = 1f; + d[21] = 1f; + + buffer.put(d); + buffer.rewind(); + + MorphTarget target = new MorphTarget(); + target.setBuffer(VertexBuffer.Type.Position, buffer); + box.addMorphTarget(target); + + + buffer = BufferUtils.createVector3Buffer(box.getVertexCount()); + + for (int i = 0; i < d.length; i++) { + d[i] = 0; + } + + d[13] = 1f; + d[16] = 1f; + d[19] = 1f; + d[22] = 1f; + + buffer.put(d); + buffer.rewind(); + + final MorphTarget target2 = new MorphTarget(); + target2.setBuffer(VertexBuffer.Type.Position, buffer); + box.addMorphTarget(target2); + + Geometry g = new Geometry("box", box); + final Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + g.setMaterial(m); + m.setColor("Color", ColorRGBA.Red); + m.setInt("NumberOfMorphTargets", 2); + + rootNode.attachChild(g); + + box.setActiveMorphTargets(0,1); + m.setParam("MorphWeights", VarType.FloatArray, weights); + + ChaseCameraAppState chase = new ChaseCameraAppState(); + chase.setTarget(rootNode); + getStateManager().attach(chase); + flyCam.setEnabled(false); + + inputManager.addMapping("morphright", new KeyTrigger(KeyInput.KEY_I)); + inputManager.addMapping("morphleft", new KeyTrigger(KeyInput.KEY_Y)); + inputManager.addMapping("morphup", new KeyTrigger(KeyInput.KEY_U)); + inputManager.addMapping("morphdown", new KeyTrigger(KeyInput.KEY_J)); + inputManager.addMapping("change", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addListener(new AnalogListener() { + @Override + public void onAnalog(String name, float value, float tpf) { + if (name.equals("morphleft")) { + weights[0] -= tpf; + } + if (name.equals("morphright")) { + weights[0] += tpf; + } + if (name.equals("morphup")) { + weights[1] += tpf; + } + if (name.equals("morphdown")) { + weights[1] -= tpf; + } + m.setParam("MorphWeights", VarType.FloatArray, weights); + + } + }, "morphup", "morphdown", "morphleft", "morphright"); + + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (name.equals("change") && isPressed) { + box.setBuffer(VertexBuffer.Type.MorphTarget0, 3, target2.getBuffer(VertexBuffer.Type.Position)); + } + } + }, "change"); + } +} 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 a6ff39d2e..f46211a51 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 @@ -11,6 +11,7 @@ import com.jme3.renderer.Camera; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.*; import com.jme3.scene.control.CameraControl; +import com.jme3.scene.mesh.MorphTarget; import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; import com.jme3.util.IntMap; @@ -19,6 +20,8 @@ import com.jme3.util.mikktspace.MikktspaceTangentGenerator; import javax.xml.bind.DatatypeConverter; import java.io.*; import java.nio.Buffer; +import java.nio.FloatBuffer; +import java.rmi.ServerError; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -128,8 +131,6 @@ public class GltfLoader implements AssetLoader { rootNode = customContentManager.readExtensionAndExtras("root", docRoot, rootNode); - setupControls(); - //Loading animations if (animations != null) { for (int i = 0; i < animations.size(); i++) { @@ -137,6 +138,8 @@ public class GltfLoader implements AssetLoader { } } + setupControls(); + //only one scene let's not return the root. if (rootNode.getChildren().size() == 1) { rootNode = (Node) rootNode.getChild(0); @@ -389,7 +392,7 @@ public class GltfLoader implements AssetLoader { //the buffers will be setup if ever used. VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight); VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex); - //setting usage to cpuOnly so that the buffer is not send empty to the GPU + //setting usage to cpuOnly so that the buffer is not sent empty to the GPU indicesHW.setUsage(VertexBuffer.Usage.CpuOnly); weightsHW.setUsage(VertexBuffer.Usage.CpuOnly); mesh.setBuffer(weightsHW); @@ -397,6 +400,22 @@ public class GltfLoader implements AssetLoader { mesh.generateBindPose(); } + JsonArray targets = meshObject.getAsJsonArray("targets"); + if(targets != null){ + for (JsonElement target : targets) { + MorphTarget morphTarget = new MorphTarget(); + for (Map.Entry entry : target.getAsJsonObject().entrySet()) { + String bufferType = entry.getKey(); + VertexBuffer.Type type = getVertexBufferType(bufferType); + VertexBuffer vb = readAccessorData(entry.getValue().getAsInt(), new VertexBufferPopulator(type)); + if (vb != null) { + morphTarget.setBuffer(type, (FloatBuffer)vb.getData()); + } + } + mesh.addMorphTarget(morphTarget); + } + } + mesh = customContentManager.readExtensionAndExtras("primitive", meshObject, mesh); Geometry geom = new Geometry(null, mesh); @@ -425,7 +444,6 @@ public class GltfLoader implements AssetLoader { geomArray[index] = geom; index++; - //TODO targets(morph anim...) } geomArray = customContentManager.readExtensionAndExtras("mesh", meshData, geomArray); @@ -721,6 +739,7 @@ public class GltfLoader implements AssetLoader { //temp data storage of track data TrackData[] tracks = new TrackData[nodes.size()]; + boolean hasMorphTrack = false; for (JsonElement channel : channels) { @@ -733,12 +752,12 @@ public class GltfLoader implements AssetLoader { continue; } assertNotNull(targetPath, "No target path for channel"); - - if (targetPath.equals("weight")) { - //Morph animation, not implemented in JME, let's warn the user and skip the channel - logger.log(Level.WARNING, "Morph animation is not supported by JME yet, skipping animation"); - continue; - } +// +// if (targetPath.equals("weights")) { +// //Morph animation, not implemented in JME, let's warn the user and skip the channel +// logger.log(Level.WARNING, "Morph animation is not supported by JME yet, skipping animation track"); +// continue; +// } TrackData trackData = tracks[targetNode]; if (trackData == null) { @@ -782,9 +801,15 @@ public class GltfLoader implements AssetLoader { Quaternion[] rotations = readAccessorData(dataIndex, quaternionArrayPopulator); trackData.rotations = rotations; } else { - //TODO support weights - logger.log(Level.WARNING, "Morph animation is not supported"); - continue; + trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Morph)); + float[] weights = readAccessorData(dataIndex, floatArrayPopulator); + Geometry g = fetchFromCache("nodes", targetNode, Geometry.class); + int expectedSize = g.getMesh().getMorphTargets().length * times.length; + if( expectedSize != weights.length ){ + throw new AssetLoadException("Morph animation should contain " + expectedSize + " entries, got" + weights.length); + } + trackData.weights = weights; + hasMorphTrack = true; } tracks[targetNode] = customContentManager.readExtensionAndExtras("channel", channel, trackData); } @@ -795,7 +820,7 @@ public class GltfLoader implements AssetLoader { List spatials = new ArrayList<>(); AnimClip anim = new AnimClip(name); - List ttracks = new ArrayList<>(); + List aTracks = new ArrayList<>(); int skinIndex = -1; List usedJoints = new ArrayList<>(); @@ -809,8 +834,22 @@ public class GltfLoader implements AssetLoader { if (node instanceof Spatial) { Spatial s = (Spatial) node; spatials.add(s); - TransformTrack track = new TransformTrack(s, trackData.times, trackData.translations, trackData.rotations, trackData.scales); - ttracks.add(track); + if (trackData.rotations != null || trackData.translations != null || trackData.scales != null) { + TransformTrack track = new TransformTrack(s, trackData.times, trackData.translations, trackData.rotations, trackData.scales); + aTracks.add(track); + } + if( trackData.weights != null && s instanceof Geometry){ + Geometry g = (Geometry)s; + int nbMorph = g.getMesh().getMorphTargets().length; +// for (int k = 0; k < trackData.weights.length; k++) { +// System.err.print(trackData.weights[k] + ","); +// if(k % nbMorph == 0 && k!=0){ +// System.err.println(" "); +// } +// } + MorphTrack track = new MorphTrack(g, trackData.times, trackData.weights, nbMorph); + aTracks.add(track); + } } else if (node instanceof JointWrapper) { JointWrapper jw = (JointWrapper) node; usedJoints.add(jw.joint); @@ -826,8 +865,7 @@ public class GltfLoader implements AssetLoader { } TransformTrack track = new TransformTrack(jw.joint, trackData.times, trackData.translations, trackData.rotations, trackData.scales); - ttracks.add(track); - + aTracks.add(track); } } @@ -845,12 +883,12 @@ public class GltfLoader implements AssetLoader { Quaternion[] rotations = new Quaternion[]{joint.getLocalRotation()}; Vector3f[] scales = new Vector3f[]{joint.getLocalScale()}; TransformTrack track = new TransformTrack(joint, times, translations, rotations, scales); - ttracks.add(track); + aTracks.add(track); } } } - anim.setTracks(ttracks.toArray(new TransformTrack[ttracks.size()])); + anim.setTracks(aTracks.toArray(new AnimTrack[aTracks.size()])); anim = customContentManager.readExtensionAndExtras("animations", animation, anim); @@ -862,11 +900,14 @@ public class GltfLoader implements AssetLoader { if (!spatials.isEmpty()) { if (skinIndex != -1) { - //there are some spatial tracks in this bone animation... or the other way around. Let's add the spatials in the skinnedSpatials. + //there are some spatial or moph tracks in this bone animation... or the other way around. Let's add the spatials in the skinnedSpatials. SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class); List spat = skinnedSpatials.get(skin); spat.addAll(spatials); //the animControl will be added in the setupControls(); + if (hasMorphTrack && skin.morphControl == null) { + skin.morphControl = new MorphControl(); + } } else { //Spatial animation Spatial spatial = null; @@ -882,6 +923,9 @@ public class GltfLoader implements AssetLoader { spatial.addControl(composer); } composer.addAnimClip(anim); + if (hasMorphTrack && spatial.getControl(MorphControl.class) == null) { + spatial.addControl(new MorphControl()); + } } } } @@ -1049,6 +1093,9 @@ public class GltfLoader implements AssetLoader { spatial.addControl(skinData.animComposer); } spatial.addControl(skinData.skinningControl); + if (skinData.morphControl != null) { + spatial.addControl(skinData.morphControl); + } } for (int i = 0; i < nodes.size(); i++) { @@ -1131,7 +1178,9 @@ public class GltfLoader implements AssetLoader { private class SkinData { SkinningControl skinningControl; + MorphControl morphControl; AnimComposer animComposer; + Spatial spatial; Spatial parent; Transform rootBoneTransformOffset; Joint[] joints; @@ -1221,6 +1270,27 @@ public class GltfLoader implements AssetLoader { } } +// +// private class FloaGridPopulator implements Populator { +// +// @Override +// public float[][] populate(Integer bufferViewIndex, int componentType, String type, int count, int byteOffset, boolean normalized) throws IOException { +// +// int numComponents = getNumberOfComponents(type); +// int dataSize = numComponents * count; +// float[] data = new float[dataSize]; +// +// if (bufferViewIndex == null) { +// //no referenced buffer, specs says to pad the data with zeros. +// padBuffer(data, dataSize); +// } else { +// readBuffer(bufferViewIndex, byteOffset, count, data, numComponents, getVertexBufferFormat(componentType)); +// } +// +// return data; +// } +// +// } private class Vector3fArrayPopulator implements Populator { 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 e32c96a6d..1e6ebfcd6 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 @@ -486,6 +486,8 @@ public class GltfUtils { mesh.setBuffer(VertexBuffer.Type.BoneIndex, 4, BufferUtils.createShortBuffer(jointsArray)); } mesh.setBuffer(VertexBuffer.Type.BoneWeight, 4, BufferUtils.createFloatBuffer(weightsArray)); + mesh.getBuffer(VertexBuffer.Type.BoneIndex).setUsage(VertexBuffer.Usage.CpuOnly); + mesh.getBuffer(VertexBuffer.Type.BoneWeight).setUsage(VertexBuffer.Usage.CpuOnly); } private static void populateFloatArray(float[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/TrackData.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/TrackData.java index b9122f072..f5e0154f3 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/TrackData.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/TrackData.java @@ -10,7 +10,8 @@ public class TrackData { public enum Type { Translation, Rotation, - Scale + Scale, + Morph } Float length; @@ -21,7 +22,6 @@ public class TrackData { Vector3f[] translations; Quaternion[] rotations; Vector3f[] scales; - //not used for now float[] weights; public void update() { @@ -125,6 +125,13 @@ public class TrackData { System.arraycopy(scales, 0, newScales, 1, scales.length); scales = newScales; } + if (weights != null) { + int nbMorph = weights.length / (times.length - 1); + float[] newWeights = new float[weights.length + nbMorph]; + System.arraycopy(weights, 0, newWeights, 0, nbMorph); + System.arraycopy(weights, 0, newWeights, nbMorph, weights.length); + weights = newWeights; + } } checkTimesConsistantcy(); @@ -336,4 +343,4 @@ public class TrackData { Vector3f scale; } -} \ No newline at end of file +} From 4048f8ba267f2fc32a3086237821647409190e1c Mon Sep 17 00:00:00 2001 From: Nehon Date: Thu, 1 Mar 2018 21:03:19 +0100 Subject: [PATCH 28/54] Adds a cpu fallback for morph animation when all morph targets cannot be handled on the gpu --- .../main/java/com/jme3/anim/MorphControl.java | 181 ++++++++++++++++-- .../jme3/anim/tween/action/ClipAction.java | 5 +- .../main/java/com/jme3/scene/Geometry.java | 43 +++++ .../src/main/java/com/jme3/scene/Mesh.java | 63 +----- .../java/com/jme3/scene/mesh/MorphTarget.java | 19 +- .../java/jme3test/model/TestGltfLoading.java | 17 +- .../anim/TestAnimMorphSerialization.java | 168 ++++++++++++++++ .../java/jme3test/model/anim/TestMorph.java | 11 +- .../jme3/scene/plugins/gltf/GltfLoader.java | 5 - 9 files changed, 405 insertions(+), 107 deletions(-) create mode 100644 jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java diff --git a/jme3-core/src/main/java/com/jme3/anim/MorphControl.java b/jme3-core/src/main/java/com/jme3/anim/MorphControl.java index 31abc8c9c..7af716b53 100644 --- a/jme3-core/src/main/java/com/jme3/anim/MorphControl.java +++ b/jme3-core/src/main/java/com/jme3/anim/MorphControl.java @@ -9,7 +9,9 @@ import com.jme3.scene.VertexBuffer; import com.jme3.scene.control.AbstractControl; import com.jme3.scene.mesh.MorphTarget; import com.jme3.shader.VarType; +import com.jme3.util.BufferUtils; import com.jme3.util.SafeArrayList; +import javafx.geometry.Pos; import java.nio.FloatBuffer; @@ -17,6 +19,7 @@ import java.nio.FloatBuffer; * A control that handle morph animation for Position, Normal and Tangent buffers. * All stock shaders only support morphing these 3 buffers, but note that MorphTargets can have any type of buffers. * If you want to use other types of buffers you will need a custom MorphControl and a custom shader. + * * @author Rémy Bouquet */ public class MorphControl extends AbstractControl { @@ -30,8 +33,17 @@ public class MorphControl extends AbstractControl { private boolean approximateTangents = true; private MatParamOverride nullNumberOfBones = new MatParamOverride(VarType.Int, "NumberOfBones", null); + private float[] tmpPosArray; + private float[] tmpNormArray; + private float[] tmpTanArray; + + private static final VertexBuffer.Type bufferTypes[] = VertexBuffer.Type.values(); + @Override protected void controlUpdate(float tpf) { + if (!enabled) { + return; + } // gathering geometries in the sub graph. // This must be done in the update phase as the gathering might add a matparam override targets.clear(); @@ -40,15 +52,18 @@ public class MorphControl extends AbstractControl { @Override protected void controlRender(RenderManager rm, ViewPort vp) { + if (!enabled) { + return; + } for (Geometry target : targets) { Mesh mesh = target.getMesh(); - if (!mesh.isDirtyMorph()) { + if (!target.isDirtyMorph()) { continue; } int nbMaxBuffers = getRemainingBuffers(mesh, rm.getRenderer()); Material m = target.getMaterial(); - float weights[] = mesh.getMorphState(); + float weights[] = target.getMorphState(); MorphTarget morphTargets[] = mesh.getMorphTargets(); float matWeights[]; MatParam param = m.getParam("MorphWeights"); @@ -69,45 +84,171 @@ public class MorphControl extends AbstractControl { m.setInt("NumberOfTargetsBuffers", targetNumBuffers); int nbGPUTargets = 0; - int nbCPUBuffers = 0; + int lastGpuTargetIndex = 0; int boundBufferIdx = 0; + float cpuWeightSum = 0; + // binding the morphTargets buffer to the mesh morph buffers for (int i = 0; i < morphTargets.length; i++) { + // discard weights below the threshold if (weights[i] < MIN_WEIGHT) { continue; } if (nbGPUTargets >= maxGPUTargets) { - //TODO we should fallback to CPU there. - nbCPUBuffers++; + // we already bound all the available gpu slots we need to merge the remaining morph targets. + cpuWeightSum += weights[i]; continue; } - int start = VertexBuffer.Type.MorphTarget0.ordinal(); + lastGpuTargetIndex = i; + // binding the morph target's buffers to the mesh morph buffers. MorphTarget t = morphTargets[i]; - if (targetNumBuffers >= 1) { - activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Position)); - boundBufferIdx++; - } - if (targetNumBuffers >= 2) { - activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Normal)); - boundBufferIdx++; - } - if (!approximateTangents && targetNumBuffers == 3) { - activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Tangent)); - boundBufferIdx++; - } + boundBufferIdx = bindMorphtargetBuffer(mesh, targetNumBuffers, boundBufferIdx, t); + // setting the weight in the mat param array matWeights[nbGPUTargets] = weights[i]; nbGPUTargets++; - } + if (nbGPUTargets < matWeights.length) { + // if we have less simultaneous GPU targets than the length of the weight array, the array is padded with 0 for (int i = nbGPUTargets; i < matWeights.length; i++) { matWeights[i] = 0; } + } else if (cpuWeightSum > 0) { + // we have more simultaneous morph targets than available gpu slots, + // we merge the additional morph targets and bind them to the last gpu slot + MorphTarget mt = target.getFallbackMorphTarget(); + if (mt == null) { + mt = initCpuMorphTarget(target); + target.setFallbackMorphTarget(mt); + } + // adding the last Gpu target weight + cpuWeightSum += matWeights[nbGPUTargets - 1]; + ensureTmpArraysCapacity(target.getVertexCount() * 3, targetNumBuffers); + + // merging all remaining targets in tmp arrays + for (int i = lastGpuTargetIndex; i < morphTargets.length; i++) { + if (weights[i] < MIN_WEIGHT) { + continue; + } + float weight = weights[i] / cpuWeightSum; + MorphTarget t = target.getMesh().getMorphTargets()[i]; + mergeMorphTargets(targetNumBuffers, weight, t, i == lastGpuTargetIndex); + } + + // writing the tmp arrays to the float buffer + writeCpuBuffer(targetNumBuffers, mt); + + // binding the merged morph target + bindMorphtargetBuffer(mesh, targetNumBuffers, (nbGPUTargets - 1) * targetNumBuffers, mt); + + // setting the eight of the merged targets + matWeights[nbGPUTargets - 1] = cpuWeightSum; } } } + private int bindMorphtargetBuffer(Mesh mesh, int targetNumBuffers, int boundBufferIdx, MorphTarget t) { + int start = VertexBuffer.Type.MorphTarget0.ordinal(); + if (targetNumBuffers >= 1) { + activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Position)); + boundBufferIdx++; + } + if (targetNumBuffers >= 2) { + activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Normal)); + boundBufferIdx++; + } + if (!approximateTangents && targetNumBuffers == 3) { + activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Tangent)); + boundBufferIdx++; + } + return boundBufferIdx; + } + + private void writeCpuBuffer(int targetNumBuffers, MorphTarget mt) { + if (targetNumBuffers >= 1) { + FloatBuffer dest = mt.getBuffer(VertexBuffer.Type.Position); + dest.rewind(); + dest.put(tmpPosArray, 0, dest.capacity()); + } + if (targetNumBuffers >= 2) { + FloatBuffer dest = mt.getBuffer(VertexBuffer.Type.Normal); + dest.rewind(); + dest.put(tmpNormArray, 0, dest.capacity()); + } + if (!approximateTangents && targetNumBuffers == 3) { + FloatBuffer dest = mt.getBuffer(VertexBuffer.Type.Tangent); + dest.rewind(); + dest.put(tmpTanArray, 0, dest.capacity()); + } + } + + private void mergeMorphTargets(int targetNumBuffers, float weight, MorphTarget t, boolean init) { + if (targetNumBuffers >= 1) { + mergeTargetBuffer(tmpPosArray, weight, t.getBuffer(VertexBuffer.Type.Position), init); + } + if (targetNumBuffers >= 2) { + mergeTargetBuffer(tmpNormArray, weight, t.getBuffer(VertexBuffer.Type.Normal), init); + } + if (!approximateTangents && targetNumBuffers == 3) { + mergeTargetBuffer(tmpTanArray, weight, t.getBuffer(VertexBuffer.Type.Tangent), init); + } + } + + private void ensureTmpArraysCapacity(int capacity, int targetNumBuffers) { + if (targetNumBuffers >= 1) { + tmpPosArray = ensureCapacity(tmpPosArray, capacity); + } + if (targetNumBuffers >= 2) { + tmpNormArray = ensureCapacity(tmpNormArray, capacity); + } + if (!approximateTangents && targetNumBuffers == 3) { + tmpTanArray = ensureCapacity(tmpTanArray, capacity); + } + } + + private void mergeTargetBuffer(float[] array, float weight, FloatBuffer src, boolean init) { + src.rewind(); + for (int j = 0; j < src.capacity(); j++) { + if (init) { + array[j] = 0; + } + array[j] += weight * src.get(); + } + } + private void activateBuffer(Mesh mesh, int idx, int start, FloatBuffer b) { - mesh.setBuffer(VertexBuffer.Type.values()[start + idx], 3, b); + VertexBuffer.Type t = bufferTypes[start + idx]; + VertexBuffer vb = mesh.getBuffer(t); + // only set the buffer if it's different + if (vb == null || vb.getData() != b) { + mesh.setBuffer(t, 3, b); + } + } + + private float[] ensureCapacity(float[] tmpArray, int size) { + if (tmpArray == null || tmpArray.length < size) { + return new float[size]; + } + return tmpArray; + } + + private MorphTarget initCpuMorphTarget(Geometry geom) { + MorphTarget res = new MorphTarget(); + MorphTarget mt = geom.getMesh().getMorphTargets()[0]; + FloatBuffer b = mt.getBuffer(VertexBuffer.Type.Position); + if (b != null) { + res.setBuffer(VertexBuffer.Type.Position, BufferUtils.createFloatBuffer(b.capacity())); + } + b = mt.getBuffer(VertexBuffer.Type.Normal); + if (b != null) { + res.setBuffer(VertexBuffer.Type.Normal, BufferUtils.createFloatBuffer(b.capacity())); + } + if (!approximateTangents) { + b = mt.getBuffer(VertexBuffer.Type.Tangent); + if (b != null) { + res.setBuffer(VertexBuffer.Type.Tangent, BufferUtils.createFloatBuffer(b.capacity())); + } + } + return res; } private int getTargetNumBuffers(MorphTarget morphTarget) { diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java index ee5920828..59d24261f 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java @@ -45,10 +45,9 @@ public class ClipAction extends BlendableAction { } private void interpolateMorphTrack(double t, MorphTrack track) { Geometry target = track.getTarget(); - float[] weights = new float[target.getMesh().getMorphTargets().length]; + float[] weights = target.getMorphState(); track.getDataAtTime(t, weights); - target.getMesh().setMorphState(weights); - + target.setMorphState(weights); // if (collectTransformDelegate != null) { // collectTransformDelegate.collectTransform(target, transform, getWeight(), this); diff --git a/jme3-core/src/main/java/com/jme3/scene/Geometry.java b/jme3-core/src/main/java/com/jme3/scene/Geometry.java index 78ea0c349..e68d75969 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Geometry.java +++ b/jme3-core/src/main/java/com/jme3/scene/Geometry.java @@ -43,6 +43,7 @@ import com.jme3.material.Material; import com.jme3.math.Matrix4f; import com.jme3.renderer.Camera; import com.jme3.scene.VertexBuffer.Type; +import com.jme3.scene.mesh.MorphTarget; import com.jme3.util.TempVars; import com.jme3.util.clone.Cloner; import com.jme3.util.clone.IdentityCloneFunction; @@ -86,6 +87,15 @@ public class Geometry extends Spatial { */ protected int startIndex = -1; + /** + * Morph state variable for morph animation + */ + private float[] morphState; + private boolean dirtyMorph = true; + // a Morph target that will be used to merge all targets that + // can't be handled on the cpu on each frame. + private MorphTarget fallbackMorphTarget; + /** * Serialization only. Do not use. */ @@ -576,6 +586,39 @@ public class Geometry extends Spatial { this.material = cloner.clone(material); } + public void setMorphState(float[] state) { + if (mesh == null || mesh.getMorphTargets().length == 0){ + return; + } + + int nbMorphTargets = mesh.getMorphTargets().length; + + if (morphState == null) { + morphState = new float[nbMorphTargets]; + } + System.arraycopy(state, 0, morphState, 0, morphState.length); + this.dirtyMorph = true; + } + + public boolean isDirtyMorph() { + return dirtyMorph; + } + + public float[] getMorphState() { + if (morphState == null) { + morphState = new float[mesh.getMorphTargets().length]; + } + return morphState; + } + + public MorphTarget getFallbackMorphTarget() { + return fallbackMorphTarget; + } + + public void setFallbackMorphTarget(MorphTarget fallbackMorphTarget) { + this.fallbackMorphTarget = fallbackMorphTarget; + } + @Override public void write(JmeExporter ex) throws IOException { super.write(ex); 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 217c38f5c..889963697 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java +++ b/jme3-core/src/main/java/com/jme3/scene/Mesh.java @@ -185,9 +185,6 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { private Mode mode = Mode.Triangles; private SafeArrayList morphTargets; - private int numMorphBuffers = 0; - private float[] morphState; - private boolean dirtyMorph = true; /** * Creates a new mesh with no {@link VertexBuffer vertex buffers}. @@ -1527,52 +1524,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { if (morphTargets == null) { morphTargets = new SafeArrayList<>(MorphTarget.class); } -// if (numMorphBuffers == 0) { -// numMorphBuffers = target.getNumBuffers(); -// int start = Type.MorphTarget0.ordinal(); -// int end = start + numMorphBuffers; -// for (int i = start; i < end; i++) { -// VertexBuffer vb = new VertexBuffer(Type.values()[i]); -// setBuffer(vb); -// } -// } else if (target.getNumBuffers() != numMorphBuffers) { -// throw new IllegalArgumentException("Morph target has different number of buffers"); -// } - morphTargets.add(target); } - public void setMorphState(float[] state) { - if (morphTargets.isEmpty()) { - return; - } - if (morphState == null) { - morphState = new float[morphTargets.size()]; - } - System.arraycopy(state, 0, morphState, 0, morphState.length); - this.dirtyMorph = true; - } - - public float[] getMorphState() { - if (morphState == null) { - morphState = new float[morphTargets.size()]; - } - return morphState; - } - - public void setActiveMorphTargets(int... targetsIndex) { - int start = Type.MorphTarget0.ordinal(); - for (int i = 0; i < targetsIndex.length; i++) { - MorphTarget t = morphTargets.get(targetsIndex[i]); - int idx = 0; - for (Type type : t.getBuffers().keySet()) { - FloatBuffer b = t.getBuffer(type); - setBuffer(Type.values()[start + i + idx], 3, b); - idx++; - } - } - } - public MorphTarget[] getMorphTargets() { return morphTargets.getArray(); } @@ -1581,21 +1535,10 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { return morphTargets != null && !morphTargets.isEmpty(); } - public boolean isDirtyMorph() { - return dirtyMorph; - } - @Override public void write(JmeExporter ex) throws IOException { OutputCapsule out = ex.getCapsule(this); -// HashMap map = new HashMap(); -// for (Entry buf : buffers){ -// if (buf.getValue() != null) -// map.put(buf.getKey()+"a", buf.getValue()); -// } -// out.writeStringSavableMap(map, "buffers", null); - out.write(meshBound, "modelBound", null); out.write(vertCount, "vertCount", -1); out.write(elementCount, "elementCount", -1); @@ -1630,6 +1573,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { } out.write(lodLevels, "lodLevels", null); + out.writeSavableArrayList(new ArrayList(morphTargets), "morphTargets", null); } @Override @@ -1669,6 +1613,11 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { lodLevels = new VertexBuffer[lodLevelsSavable.length]; System.arraycopy( lodLevelsSavable, 0, lodLevels, 0, lodLevels.length); } + + ArrayList l = in.readSavableArrayList("morphTargets", null); + if (l != null) { + morphTargets = new SafeArrayList(MorphTarget.class, l); + } } } diff --git a/jme3-core/src/main/java/com/jme3/scene/mesh/MorphTarget.java b/jme3-core/src/main/java/com/jme3/scene/mesh/MorphTarget.java index 8f65473a2..083d04fb7 100644 --- a/jme3-core/src/main/java/com/jme3/scene/mesh/MorphTarget.java +++ b/jme3-core/src/main/java/com/jme3/scene/mesh/MorphTarget.java @@ -1,13 +1,13 @@ package com.jme3.scene.mesh; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.Savable; +import com.jme3.export.*; import com.jme3.scene.VertexBuffer; import java.io.IOException; +import java.nio.Buffer; import java.nio.FloatBuffer; import java.util.EnumMap; +import java.util.Map; public class MorphTarget implements Savable { private EnumMap buffers = new EnumMap<>(VertexBuffer.Type.class); @@ -30,11 +30,22 @@ public class MorphTarget implements Savable { @Override public void write(JmeExporter ex) throws IOException { - + OutputCapsule oc = ex.getCapsule(this); + for (Map.Entry entry : buffers.entrySet()) { + Buffer roData = entry.getValue().asReadOnlyBuffer(); + oc.write((FloatBuffer) roData, entry.getKey().name(),null); + } } @Override public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + for (VertexBuffer.Type type : VertexBuffer.Type.values()) { + FloatBuffer b = ic.readFloatBuffer(type.name(), null); + if(b!= null){ + setBuffer(type, b); + } + } } } diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index be2908194..61219cae2 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -33,8 +33,7 @@ package jme3test.model; import com.jme3.anim.AnimComposer; import com.jme3.anim.SkinningControl; -import com.jme3.app.ChaseCameraAppState; -import com.jme3.app.SimpleApplication; +import com.jme3.app.*; import com.jme3.asset.plugins.FileLocator; import com.jme3.input.KeyInput; import com.jme3.input.controls.ActionListener; @@ -45,7 +44,6 @@ import com.jme3.scene.*; import com.jme3.scene.control.Control; import com.jme3.scene.debug.custom.ArmatureDebugAppState; import com.jme3.scene.plugins.gltf.GltfModelKey; -import com.jme3.shader.VarType; import jme3test.model.anim.EraseTimer; import java.util.*; @@ -118,8 +116,8 @@ public class TestGltfLoading extends SimpleApplication { // rootNode.addLight(pl1); //loadModel("Models/gltf/polly/project_polly.gltf", new Vector3f(0, 0, 0), 0.5f); - //loadModel("Models/gltf/zophrac/scene.gltf", new Vector3f(0, 0, 0), 0.1f); - loadModel("Models/gltf/scifigirl/scene.gltf", new Vector3f(0, -1, 0), 0.1f); + loadModel("Models/gltf/zophrac/scene.gltf", new Vector3f(0, 0, 0), 0.1f); + // loadModel("Models/gltf/scifigirl/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/man/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/torus/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/morph/scene.gltf", new Vector3f(0, 0, 0), 0.2f); @@ -214,15 +212,8 @@ public class TestGltfLoading extends SimpleApplication { }, "nextAnim"); dumpScene(rootNode, 0); - } - public void setMorphTarget(int index) { - g = (Geometry) probeNode.getChild("0"); - g.getMesh().setActiveMorphTargets(index); - g.getMaterial().setInt("NumberOfMorphTargets", 1); - g.getMaterial().setInt("NumberOfTargetsBuffers", 3); - float[] weights = {1.0f}; - g.getMaterial().setParam("MorphWeights", VarType.FloatArray, weights); + stateManager.attach(new DetailedProfilerState()); } private T findControl(Spatial s, Class controlClass) { diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java new file mode 100644 index 000000000..1d3551df2 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java @@ -0,0 +1,168 @@ +package jme3test.model.anim; + +import com.jme3.anim.AnimComposer; +import com.jme3.anim.SkinningControl; +import com.jme3.anim.util.AnimMigrationUtils; +import com.jme3.app.ChaseCameraAppState; +import com.jme3.app.SimpleApplication; +import com.jme3.asset.plugins.FileLocator; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.math.*; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.debug.custom.ArmatureDebugAppState; +import com.jme3.system.JmeSystem; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.Queue; + +/** + * Created by Nehon on 18/12/2017. + */ +public class TestAnimSerialization extends SimpleApplication { + + ArmatureDebugAppState debugAppState; + AnimComposer composer; + Queue anims = new LinkedList<>(); + boolean playAnim = true; + File file; + + public static void main(String... argv) { + TestAnimSerialization app = new TestAnimSerialization(); + app.start(); + } + + @Override + public void simpleInitApp() { + setTimer(new EraseTimer()); + //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); + rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); + + Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + + AnimMigrationUtils.migrate(model); + + File storageFolder = JmeSystem.getStorageFolder(); + file = new File(storageFolder.getPath() + File.separator + "newJaime.j3o"); + BinaryExporter be = new BinaryExporter(); + try { + be.save(model, file); + } catch (IOException e) { + e.printStackTrace(); + } + + assetManager.registerLocator(storageFolder.getPath(), FileLocator.class); + model = assetManager.loadModel("newJaime.j3o"); + + rootNode.attachChild(model); + + debugAppState = new ArmatureDebugAppState(); + stateManager.attach(debugAppState); + + setupModel(model); + + flyCam.setEnabled(false); + + Node target = new Node("CamTarget"); + //target.setLocalTransform(model.getLocalTransform()); + target.move(0, 1, 0); + ChaseCameraAppState chaseCam = new ChaseCameraAppState(); + chaseCam.setTarget(target); + getStateManager().attach(chaseCam); + chaseCam.setInvertHorizontalAxis(true); + chaseCam.setInvertVerticalAxis(true); + chaseCam.setZoomSpeed(0.5f); + chaseCam.setMinVerticalRotation(-FastMath.HALF_PI); + chaseCam.setRotationSpeed(3); + chaseCam.setDefaultDistance(3); + chaseCam.setMinDistance(0.01f); + chaseCam.setZoomSpeed(0.01f); + chaseCam.setDefaultVerticalRotation(0.3f); + + initInputs(); + } + + public void initInputs() { + inputManager.addMapping("toggleAnim", new KeyTrigger(KeyInput.KEY_RETURN)); + + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed) { + playAnim = !playAnim; + if (playAnim) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAction(anim); + System.err.println(anim); + } else { + composer.reset(); + } + } + } + }, "toggleAnim"); + inputManager.addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_RIGHT)); + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed && composer != null) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAction(anim); + System.err.println(anim); + } + } + }, "nextAnim"); + } + + private void setupModel(Spatial model) { + if (composer != null) { + return; + } + composer = model.getControl(AnimComposer.class); + if (composer != null) { + + SkinningControl sc = model.getControl(SkinningControl.class); + + debugAppState.addArmatureFrom(sc); + anims.clear(); + for (String name : composer.getAnimClipsNames()) { + anims.add(name); + } + if (anims.isEmpty()) { + return; + } + if (playAnim) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAction(anim); + System.err.println(anim); + } + + } else { + if (model instanceof Node) { + Node n = (Node) model; + for (Spatial child : n.getChildren()) { + setupModel(child); + } + } + } + + } + + + @Override + public void destroy() { + super.destroy(); + file.delete(); + } +} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestMorph.java b/jme3-examples/src/main/java/jme3test/model/anim/TestMorph.java index d02ff40c1..629046685 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestMorph.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestMorph.java @@ -1,5 +1,6 @@ package jme3test.model.anim; +import com.jme3.anim.MorphControl; import com.jme3.app.ChaseCameraAppState; import com.jme3.app.SimpleApplication; import com.jme3.input.KeyInput; @@ -68,16 +69,16 @@ public class TestMorph extends SimpleApplication { target2.setBuffer(VertexBuffer.Type.Position, buffer); box.addMorphTarget(target2); - Geometry g = new Geometry("box", box); - final Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + final Geometry g = new Geometry("box", box); + Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); g.setMaterial(m); m.setColor("Color", ColorRGBA.Red); m.setInt("NumberOfMorphTargets", 2); rootNode.attachChild(g); - box.setActiveMorphTargets(0,1); - m.setParam("MorphWeights", VarType.FloatArray, weights); + g.setMorphState(weights); + g.addControl(new MorphControl()); ChaseCameraAppState chase = new ChaseCameraAppState(); chase.setTarget(rootNode); @@ -104,7 +105,7 @@ public class TestMorph extends SimpleApplication { if (name.equals("morphdown")) { weights[1] -= tpf; } - m.setParam("MorphWeights", VarType.FloatArray, weights); + g.setMorphState(weights); } }, "morphup", "morphdown", "morphleft", "morphright"); 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 f46211a51..fce2bf8ad 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 @@ -803,11 +803,6 @@ public class GltfLoader implements AssetLoader { } else { trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Morph)); float[] weights = readAccessorData(dataIndex, floatArrayPopulator); - Geometry g = fetchFromCache("nodes", targetNode, Geometry.class); - int expectedSize = g.getMesh().getMorphTargets().length * times.length; - if( expectedSize != weights.length ){ - throw new AssetLoadException("Morph animation should contain " + expectedSize + " entries, got" + weights.length); - } trackData.weights = weights; hasMorphTrack = true; } From daba9b8bc745700e4b36dd27e04c0c06aafd6034 Mon Sep 17 00:00:00 2001 From: Nehon Date: Sat, 3 Mar 2018 16:18:25 +0100 Subject: [PATCH 29/54] Proper serialisation for morph animation --- .../src/main/java/com/jme3/anim/AnimClip.java | 4 +- .../src/main/java/com/jme3/anim/Armature.java | 37 ++++-- .../src/main/java/com/jme3/anim/Joint.java | 40 ++++++- .../main/java/com/jme3/anim/MorphControl.java | 107 +++++++++++++----- .../main/java/com/jme3/anim/MorphTrack.java | 2 + .../java/com/jme3/anim/TransformTrack.java | 2 +- .../jme3/anim/util/AnimMigrationUtils.java | 2 +- .../java/com/jme3/animation/BoneTrack.java | 4 - .../com/jme3/material/MatParamOverride.java | 7 ++ .../main/java/com/jme3/material/Material.java | 8 +- .../java/com/jme3/renderer/RenderManager.java | 2 +- .../main/java/com/jme3/scene/Geometry.java | 44 ++++++- .../src/main/java/com/jme3/scene/Mesh.java | 4 +- .../java/com/jme3/scene/VertexBuffer.java | 9 +- .../java/jme3test/model/TestGltfLoading.java | 10 +- .../anim/TestAnimMorphSerialization.java | 36 +++--- .../jme3test/model/anim/TestArmature.java | 4 +- .../model/anim/TestBaseAnimSerialization.java | 4 +- .../jme3/scene/plugins/gltf/GltfLoader.java | 1 + .../scene/plugins/ogre/SkeletonLoader.java | 2 +- 20 files changed, 244 insertions(+), 85 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java index 0baf7b873..7f8c022c5 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java @@ -80,9 +80,9 @@ public class AnimClip implements JmeCloneable, Savable { name = ic.readString("name", null); Savable[] arr = ic.readSavableArray("tracks", null); if (arr != null) { - tracks = new TransformTrack[arr.length]; + tracks = new AnimTrack[arr.length]; for (int i = 0; i < arr.length; i++) { - TransformTrack t = (TransformTrack) arr[i]; + AnimTrack t = (AnimTrack) arr[i]; tracks[i] = t; if (t.getLength() > length) { length = t.getLength(); diff --git a/jme3-core/src/main/java/com/jme3/anim/Armature.java b/jme3-core/src/main/java/com/jme3/anim/Armature.java index 582dc8bce..39a664ede 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Armature.java +++ b/jme3-core/src/main/java/com/jme3/anim/Armature.java @@ -174,28 +174,49 @@ public class Armature implements JmeCloneable, Savable { /** * Saves the current Armature state as its bind pose. + * Note that the bind pose is supposed to be the one where the armature is aligned with the mesh to deform. + * Saving this pose will affect how skinning works. */ - public void setBindPose() { + public void saveBindPose() { //make sure all bones are updated update(); //Save the current pose as bind pose for (Joint joint : jointList) { - joint.setBindPose(); + joint.saveBindPose(); } } /** - * This methods sets this armature in its bind pose (aligned with the undeformed mesh) - * Note that this is only useful for debugging porpose. + * This methods sets this armature in its bind pose (aligned with the mesh to deform) + * Note that this is only useful for debugging purpose. */ - public void resetToBindPose() { + public void applyBindPose() { for (Joint joint : rootJoints) { - joint.resetToBindPose(); + joint.applyBindPose(); } } /** - * Compute the skining matrices for each bone of the armature that would be used to transform vertices of associated meshes + * Saves the current local transform as the initial transform. + * Initial transform is the one applied to the armature when loaded. + */ + public void saveInitialPose() { + for (Joint joint : jointList) { + joint.saveInitialPose(); + } + } + + /** + * Applies the initial pose to this armature + */ + public void applyInitialPose() { + for (Joint rootJoint : rootJoints) { + rootJoint.applyInitialPose(); + } + } + + /** + * Compute the skinning matrices for each bone of the armature that would be used to transform vertices of associated meshes * * @return */ @@ -263,7 +284,7 @@ public class Armature implements JmeCloneable, Savable { for (Joint rootJoint : rootJoints) { rootJoint.update(); } - resetToBindPose(); + applyInitialPose(); } @Override diff --git a/jme3-core/src/main/java/com/jme3/anim/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java index eaa7ac145..f4c0a0d1b 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Joint.java +++ b/jme3-core/src/main/java/com/jme3/anim/Joint.java @@ -37,6 +37,13 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform { */ private Transform localTransform = new Transform(); + /** + * The initial transform of the joint in local space. Relative to its parent. + * Or relative to the model's origin for the root joint. + * this transform is the transform applied when the armature is loaded. + */ + private Transform initialTransform = new Transform(); + /** * The transform of the joint in model space. Relative to the origin of the model. * this is either a MatrixJointModelTransform or a SeparateJointModelTransform @@ -127,18 +134,43 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform { jointModelTransform.getOffsetTransform(outTransform, inverseModelBindMatrix); } - protected void setBindPose() { + /** + * Sets the current localTransform as the Bind transform. + */ + protected void saveBindPose() { //Note that the whole Armature must be updated before calling this method. getModelTransform().toTransformMatrix(inverseModelBindMatrix); inverseModelBindMatrix.invertLocal(); } - protected void resetToBindPose() { + /** + * Sets the current local transforms as the initial transform. + */ + protected void saveInitialPose() { + initialTransform.set(localTransform); + } + + /** + * Sets the local transform with the bind transforms + */ + protected void applyBindPose() { jointModelTransform.applyBindPose(localTransform, inverseModelBindMatrix, parent); updateModelTransforms(); for (Joint child : children.getArray()) { - child.resetToBindPose(); + child.applyBindPose(); + } + } + + /** + * Sets the local transform with the initial transform + */ + protected void applyInitialPose() { + setLocalTransform(initialTransform); + updateModelTransforms(); + + for (Joint child : children.getArray()) { + child.applyInitialPose(); } } @@ -277,6 +309,7 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform { name = input.readString("name", null); attachedNode = (Node) input.readSavable("attachedNode", null); targetGeometry = (Geometry) input.readSavable("targetGeometry", null); + initialTransform = (Transform) input.readSavable("initialTransform", new Transform()); inverseModelBindMatrix = (Matrix4f) input.readSavable("inverseModelBindMatrix", inverseModelBindMatrix); ArrayList childList = input.readSavableArrayList("children", null); @@ -292,6 +325,7 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform { output.write(name, "name", null); output.write(attachedNode, "attachedNode", null); output.write(targetGeometry, "targetGeometry", null); + output.write(initialTransform, "initialTransform", new Transform()); output.write(inverseModelBindMatrix, "inverseModelBindMatrix", new Matrix4f()); output.writeSavableArrayList(new ArrayList(children), "children", null); } diff --git a/jme3-core/src/main/java/com/jme3/anim/MorphControl.java b/jme3-core/src/main/java/com/jme3/anim/MorphControl.java index 7af716b53..80398ce6c 100644 --- a/jme3-core/src/main/java/com/jme3/anim/MorphControl.java +++ b/jme3-core/src/main/java/com/jme3/anim/MorphControl.java @@ -1,11 +1,9 @@ package com.jme3.anim; +import com.jme3.export.*; import com.jme3.material.*; import com.jme3.renderer.*; -import com.jme3.scene.Geometry; -import com.jme3.scene.Mesh; -import com.jme3.scene.SceneGraphVisitorAdapter; -import com.jme3.scene.VertexBuffer; +import com.jme3.scene.*; import com.jme3.scene.control.AbstractControl; import com.jme3.scene.mesh.MorphTarget; import com.jme3.shader.VarType; @@ -13,7 +11,10 @@ import com.jme3.util.BufferUtils; import com.jme3.util.SafeArrayList; import javafx.geometry.Pos; +import java.io.IOException; import java.nio.FloatBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; /** * A control that handle morph animation for Position, Normal and Tangent buffers. @@ -22,7 +23,9 @@ import java.nio.FloatBuffer; * * @author Rémy Bouquet */ -public class MorphControl extends AbstractControl { +public class MorphControl extends AbstractControl implements Savable { + + private static final Logger logger = Logger.getLogger(MorphControl.class.getName()); private static final int MAX_MORPH_BUFFERS = 14; private final static float MIN_WEIGHT = 0.005f; @@ -55,33 +58,23 @@ public class MorphControl extends AbstractControl { if (!enabled) { return; } - for (Geometry target : targets) { - Mesh mesh = target.getMesh(); - if (!target.isDirtyMorph()) { + for (Geometry geom : targets) { + Mesh mesh = geom.getMesh(); + if (!geom.isDirtyMorph()) { continue; } - int nbMaxBuffers = getRemainingBuffers(mesh, rm.getRenderer()); - Material m = target.getMaterial(); - float weights[] = target.getMorphState(); + Material m = geom.getMaterial(); + float weights[] = geom.getMorphState(); MorphTarget morphTargets[] = mesh.getMorphTargets(); float matWeights[]; - MatParam param = m.getParam("MorphWeights"); - //Number of buffer to handle for each morph target int targetNumBuffers = getTargetNumBuffers(morphTargets[0]); - // compute the max number of targets to send to the GPU - int maxGPUTargets = Math.min(nbMaxBuffers, MAX_MORPH_BUFFERS) / targetNumBuffers; - if (param == null) { - matWeights = new float[maxGPUTargets]; - m.setParam("MorphWeights", VarType.FloatArray, matWeights); - } else { - matWeights = (float[]) param.getValue(); - } - // setting the maximum number as the real number may change every frame and trigger a shader recompilation since it's bound to a define. - m.setInt("NumberOfMorphTargets", maxGPUTargets); - m.setInt("NumberOfTargetsBuffers", targetNumBuffers); + int maxGPUTargets = getMaxGPUTargets(rm, geom, m, targetNumBuffers); + + MatParam param2 = m.getParam("MorphWeights"); + matWeights = (float[]) param2.getValue(); int nbGPUTargets = 0; int lastGpuTargetIndex = 0; @@ -115,14 +108,14 @@ public class MorphControl extends AbstractControl { } else if (cpuWeightSum > 0) { // we have more simultaneous morph targets than available gpu slots, // we merge the additional morph targets and bind them to the last gpu slot - MorphTarget mt = target.getFallbackMorphTarget(); + MorphTarget mt = geom.getFallbackMorphTarget(); if (mt == null) { - mt = initCpuMorphTarget(target); - target.setFallbackMorphTarget(mt); + mt = initCpuMorphTarget(geom); + geom.setFallbackMorphTarget(mt); } // adding the last Gpu target weight cpuWeightSum += matWeights[nbGPUTargets - 1]; - ensureTmpArraysCapacity(target.getVertexCount() * 3, targetNumBuffers); + ensureTmpArraysCapacity(geom.getVertexCount() * 3, targetNumBuffers); // merging all remaining targets in tmp arrays for (int i = lastGpuTargetIndex; i < morphTargets.length; i++) { @@ -130,7 +123,7 @@ public class MorphControl extends AbstractControl { continue; } float weight = weights[i] / cpuWeightSum; - MorphTarget t = target.getMesh().getMorphTargets()[i]; + MorphTarget t = geom.getMesh().getMorphTargets()[i]; mergeMorphTargets(targetNumBuffers, weight, t, i == lastGpuTargetIndex); } @@ -143,7 +136,52 @@ public class MorphControl extends AbstractControl { // setting the eight of the merged targets matWeights[nbGPUTargets - 1] = cpuWeightSum; } + geom.setDirtyMorph(false); + } + } + + private int getMaxGPUTargets(RenderManager rm, Geometry geom, Material mat, int targetNumBuffers) { + if (geom.getNbSimultaneousGPUMorph() > -1) { + return geom.getNbSimultaneousGPUMorph(); + } + + // Evaluate the number of CPU slots remaining for morph buffers. + int nbMaxBuffers = getRemainingBuffers(geom.getMesh(), rm.getRenderer()); + + int realNumTargetsBuffers = geom.getMesh().getMorphTargets().length * targetNumBuffers; + + // compute the max number of targets to send to the GPU + int maxGPUTargets = Math.min(realNumTargetsBuffers, Math.min(nbMaxBuffers, MAX_MORPH_BUFFERS)) / targetNumBuffers; + + MatParam param = mat.getParam("MorphWeights"); + if (param == null) { + // init the mat param if it doesn't exists. + float[] wts = new float[maxGPUTargets]; + mat.setParam("MorphWeights", VarType.FloatArray, wts); + } + + mat.setInt("NumberOfTargetsBuffers", targetNumBuffers); + + // test compile the shader to find the accurate number of remaining attributes slots + boolean compilationOk = false; + // Note that if ever the shader has an unrelated issue we want to break at some point, hence the maxGPUTargets > 0 + while (!compilationOk && maxGPUTargets > 0) { + // setting the maximum number as the real number may change every frame and trigger a shader recompilation since it's bound to a define. + mat.setInt("NumberOfMorphTargets", maxGPUTargets); + try { + // preload the spatial. this will trigger a shader compilation that will fail if the number of attributes is over the limit. + rm.preloadScene(spatial); + compilationOk = true; + } catch (RendererException e) { + logger.log(Level.FINE, geom.getName() + ": failed at " + maxGPUTargets); + // the compilation failed let's decrement the number of targets an try again. + maxGPUTargets--; + } } + logger.log(Level.FINE, geom.getName() + ": " + maxGPUTargets); + // set the number of GPU morph on the geom to not have to recompute it next frame. + geom.setNbSimultaneousGPUMorph(maxGPUTargets); + return maxGPUTargets; } private int bindMorphtargetBuffer(Mesh mesh, int targetNumBuffers, int boundBufferIdx, MorphTarget t) { @@ -263,6 +301,17 @@ public class MorphControl extends AbstractControl { return num; } + /** + * Computes the number of remaining buffers on this mesh. + * This is supposed to give a hint on how many attributes will be used in the material and computes the remaining available slots for the morph attributes. + * However, the shader can declare attributes that are not used and not bound to a real buffer. + * That's why we attempt to compile the shader later on to avoid any compilation crash. + * This method is here to avoid too much render test iteration. + * + * @param mesh + * @param renderer + * @return + */ private int getRemainingBuffers(Mesh mesh, Renderer renderer) { int nbUsedBuffers = 0; for (VertexBuffer vb : mesh.getBufferList().getArray()) { diff --git a/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java b/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java index b1968df36..cd662c11e 100644 --- a/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java +++ b/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java @@ -187,6 +187,7 @@ public class MorphTrack implements AnimTrack { oc.write(weights, "weights", null); oc.write(times, "times", null); oc.write(target, "target", null); + oc.write(nbMorphTargets, "nbMorphTargets", 0); } @Override @@ -195,6 +196,7 @@ public class MorphTrack implements AnimTrack { weights = ic.readFloatArray("weights", null); times = ic.readFloatArray("times", null); target = (Geometry) ic.readSavable("target", null); + nbMorphTargets = ic.readInt("nbMorphTargets", 0); setTimes(times); } diff --git a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java index b5e5fb85f..dee2aefb9 100644 --- a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java +++ b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java @@ -294,7 +294,7 @@ public class TransformTrack implements AnimTrack { rotations = (CompactQuaternionArray) ic.readSavable("rotations", null); times = ic.readFloatArray("times", null); scales = (CompactVector3Array) ic.readSavable("scales", null); - target = (Joint) ic.readSavable("target", null); + target = (HasLocalTransform) ic.readSavable("target", null); setTimes(times); } diff --git a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java index c9e65b08f..d50bf90b8 100644 --- a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java +++ b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java @@ -58,7 +58,7 @@ public class AnimMigrationUtils { } Armature armature = new Armature(joints); - armature.setBindPose(); + armature.saveBindPose(); skeletonArmatureMap.put(skeleton, armature); List tracks = new ArrayList<>(); diff --git a/jme3-core/src/main/java/com/jme3/animation/BoneTrack.java b/jme3-core/src/main/java/com/jme3/animation/BoneTrack.java index 32f5e6b7b..a39108c5a 100644 --- a/jme3-core/src/main/java/com/jme3/animation/BoneTrack.java +++ b/jme3-core/src/main/java/com/jme3/animation/BoneTrack.java @@ -35,12 +35,8 @@ import com.jme3.export.*; import com.jme3.math.Quaternion; import com.jme3.math.Vector3f; import com.jme3.util.TempVars; -<<<<<<< HEAD import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; -======= - ->>>>>>> Draft of the new animation system import java.io.IOException; import java.util.BitSet; diff --git a/jme3-core/src/main/java/com/jme3/material/MatParamOverride.java b/jme3-core/src/main/java/com/jme3/material/MatParamOverride.java index 8a7355b87..1b4aad480 100644 --- a/jme3-core/src/main/java/com/jme3/material/MatParamOverride.java +++ b/jme3-core/src/main/java/com/jme3/material/MatParamOverride.java @@ -140,6 +140,9 @@ public final class MatParamOverride extends MatParam { super.write(ex); OutputCapsule oc = ex.getCapsule(this); oc.write(enabled, "enabled", true); + if (value == null) { + oc.write(true, "isNull", false); + } } @Override @@ -147,5 +150,9 @@ public final class MatParamOverride extends MatParam { super.read(im); InputCapsule ic = im.getCapsule(this); enabled = ic.readBoolean("enabled", true); + boolean isNull = ic.readBoolean("isNull", false); + if (isNull) { + setValue(null); + } } } diff --git a/jme3-core/src/main/java/com/jme3/material/Material.java b/jme3-core/src/main/java/com/jme3/material/Material.java index a14bc357d..e2b030e93 100644 --- a/jme3-core/src/main/java/com/jme3/material/Material.java +++ b/jme3-core/src/main/java/com/jme3/material/Material.java @@ -836,7 +836,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { * * @param renderManager The render manager to preload for */ - public void preload(RenderManager renderManager) { + public void preload(RenderManager renderManager, Geometry geometry) { if (technique == null) { selectTechnique(TechniqueDef.DEFAULT_TECHNIQUE_NAME, renderManager); } @@ -847,9 +847,11 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { if (techniqueDef.isNoRender()) { return; } + // Get world overrides + SafeArrayList overrides = geometry.getWorldMatParamOverrides(); - Shader shader = technique.makeCurrent(renderManager, null, null, null, rendererCaps); - updateShaderMaterialParameters(renderer, shader, null, null); + Shader shader = technique.makeCurrent(renderManager, overrides, null, null, rendererCaps); + updateShaderMaterialParameters(renderer, shader, overrides, null); renderManager.getRenderer().setShader(shader); } diff --git a/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java b/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java index 238445b6c..cc86b5ad4 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java @@ -660,7 +660,7 @@ public class RenderManager { throw new IllegalStateException("No material is set for Geometry: " + gm.getName()); } - gm.getMaterial().preload(this); + gm.getMaterial().preload(this, gm); Mesh mesh = gm.getMesh(); if (mesh != null && mesh.getVertexCount() != 0 diff --git a/jme3-core/src/main/java/com/jme3/scene/Geometry.java b/jme3-core/src/main/java/com/jme3/scene/Geometry.java index e68d75969..437036222 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Geometry.java +++ b/jme3-core/src/main/java/com/jme3/scene/Geometry.java @@ -95,6 +95,7 @@ public class Geometry extends Spatial { // a Morph target that will be used to merge all targets that // can't be handled on the cpu on each frame. private MorphTarget fallbackMorphTarget; + private int nbSimultaneousGPUMorph = -1; /** * Serialization only. Do not use. @@ -258,7 +259,7 @@ public class Geometry extends Spatial { @Override public void setMaterial(Material material) { this.material = material; - + nbSimultaneousGPUMorph = -1; if (isGrouped()) { groupNode.onMaterialChange(this); } @@ -600,10 +601,28 @@ public class Geometry extends Spatial { this.dirtyMorph = true; } + /** + * returns true if the morph state has changed on the last frame. + * @return + */ public boolean isDirtyMorph() { return dirtyMorph; } + /** + * Seting this to true will stop this geometry morph buffer to be updated, + * unless the morph state changes + * @param dirtyMorph + */ + public void setDirtyMorph(boolean dirtyMorph) { + this.dirtyMorph = dirtyMorph; + } + + /** + * returns the morph state of this Geometry. + * Used internally by the MorphControl. + * @return + */ public float[] getMorphState() { if (morphState == null) { morphState = new float[mesh.getMorphTargets().length]; @@ -611,6 +630,29 @@ public class Geometry extends Spatial { return morphState; } + /** + * Return the number of morph targets that can be handled on the GPU simultaneously for this geometry. + * Note that it depends on the material set on this geometry. + * This number is computed and set by the MorphControl, so it might be available only after the first frame. + * Else it's set to -1. + * @return the number of simultaneous morph targets handled on the GPU + */ + public int getNbSimultaneousGPUMorph() { + return nbSimultaneousGPUMorph; + } + + /** + * Sets the number of morph targets that can be handled on the GPU simultaneously for this geometry. + * Note that it depends on the material set on this geometry. + * This number is computed and set by the MorphControl, so it might be available only after the first frame. + * Else it's set to -1. + * WARNING: setting this manually might crash the shader compilation if set too high. Do it at your own risk. + * @param nbSimultaneousGPUMorph the number of simultaneous morph targets to be handled on the GPU. + */ + public void setNbSimultaneousGPUMorph(int nbSimultaneousGPUMorph) { + this.nbSimultaneousGPUMorph = nbSimultaneousGPUMorph; + } + public MorphTarget getFallbackMorphTarget() { return fallbackMorphTarget; } 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 889963697..8dececf26 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java +++ b/jme3-core/src/main/java/com/jme3/scene/Mesh.java @@ -1573,7 +1573,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { } out.write(lodLevels, "lodLevels", null); - out.writeSavableArrayList(new ArrayList(morphTargets), "morphTargets", null); + if (morphTargets != null) { + out.writeSavableArrayList(new ArrayList(morphTargets), "morphTargets", null); + } } @Override diff --git a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java index 9753850a1..bb5d70afb 100644 --- a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java +++ b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java @@ -216,15 +216,16 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { /** * Morph animations targets. - * Supports up tp 8 morph target buffers at the same time + * Supports up tp 14 morph target buffers at the same time * Limited due to the limited number of attributes you can bind to a vertex shader usually 16 *

* MorphTarget buffers are either POSITION, NORMAL or TANGENT buffers. * So we can support up to - * 10 simultaneous POSITION targets - * 5 simultaneous POSITION and NORMAL targets - * 3 simultaneous POSTION, NORMAL and TANGENT targets. + * 14 simultaneous POSITION targets + * 7 simultaneous POSITION and NORMAL targets + * 4 simultaneous POSTION, NORMAL and TANGENT targets. *

+ * Note that the MorphControl will find how many buffers can be supported for each mesh/material combination. * Note that all buffers have 3 components (Vector3f) even the Tangent buffer that * does not contain the w (handedness) component that will not be interpolated for morph animation. *

diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index 61219cae2..8aa044d55 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -116,15 +116,15 @@ public class TestGltfLoading extends SimpleApplication { // rootNode.addLight(pl1); //loadModel("Models/gltf/polly/project_polly.gltf", new Vector3f(0, 0, 0), 0.5f); - loadModel("Models/gltf/zophrac/scene.gltf", new Vector3f(0, 0, 0), 0.1f); + //loadModel("Models/gltf/zophrac/scene.gltf", new Vector3f(0, 0, 0), 0.1f); // loadModel("Models/gltf/scifigirl/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/man/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/torus/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/morph/scene.gltf", new Vector3f(0, 0, 0), 0.2f); //loadModel("Models/gltf/morphCube/AnimatedMorphCube.gltf", new Vector3f(0, 0, 0), 1f); - // loadModel("Models/gltf/morph/SimpleMorph.gltf", new Vector3f(0, 0, 0), 0.1f); + // loadModel("Models/gltf/morph/SimpleMorph.gltf", new Vector3f(0, 0, 0), 0.1f); //loadModel("Models/gltf/nier/scene.gltf", new Vector3f(0, -1.5f, 0), 0.01f); - //loadModel("Models/gltf/izzy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + loadModel("Models/gltf/izzy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/darth/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/mech/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/elephant/scene.gltf", new Vector3f(0, -1, 0), 0.01f); @@ -132,7 +132,7 @@ public class TestGltfLoading extends SimpleApplication { //loadModel("Models/gltf/war/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/ganjaarl/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/hero/scene.gltf", new Vector3f(0, -1, 0), 0.1f); - /// loadModel("Models/gltf/mercy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + //loadModel("Models/gltf/mercy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/crab/scene.gltf", Vector3f.ZERO, 1); //loadModel("Models/gltf/manta/scene.gltf", Vector3f.ZERO, 0.2f); //loadModel("Models/gltf/bone/scene.gltf", Vector3f.ZERO, 0.1f); @@ -213,7 +213,7 @@ public class TestGltfLoading extends SimpleApplication { dumpScene(rootNode, 0); - stateManager.attach(new DetailedProfilerState()); + // stateManager.attach(new DetailedProfilerState()); } private T findControl(Spatial s, Class controlClass) { diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java index 1d3551df2..2e122a35e 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java @@ -1,7 +1,6 @@ package jme3test.model.anim; -import com.jme3.anim.AnimComposer; -import com.jme3.anim.SkinningControl; +import com.jme3.anim.*; import com.jme3.anim.util.AnimMigrationUtils; import com.jme3.app.ChaseCameraAppState; import com.jme3.app.SimpleApplication; @@ -26,7 +25,7 @@ import java.util.Queue; /** * Created by Nehon on 18/12/2017. */ -public class TestAnimSerialization extends SimpleApplication { +public class TestAnimMorphSerialization extends SimpleApplication { ArmatureDebugAppState debugAppState; AnimComposer composer; @@ -35,7 +34,7 @@ public class TestAnimSerialization extends SimpleApplication { File file; public static void main(String... argv) { - TestAnimSerialization app = new TestAnimSerialization(); + TestAnimMorphSerialization app = new TestAnimMorphSerialization(); app.start(); } @@ -44,15 +43,14 @@ public class TestAnimSerialization extends SimpleApplication { setTimer(new EraseTimer()); //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); viewPort.setBackgroundColor(ColorRGBA.DarkGray); - rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); - rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); - - Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); - - AnimMigrationUtils.migrate(model); + //rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); + //rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); + Node probeNode = (Node) assetManager.loadModel("Scenes/defaultProbe.j3o"); + rootNode.attachChild(probeNode); + Spatial model = assetManager.loadModel("Models/gltf/zophrac/scene.gltf"); File storageFolder = JmeSystem.getStorageFolder(); - file = new File(storageFolder.getPath() + File.separator + "newJaime.j3o"); + file = new File(storageFolder.getPath() + File.separator + "zophrac.j3o"); BinaryExporter be = new BinaryExporter(); try { be.save(model, file); @@ -61,20 +59,20 @@ public class TestAnimSerialization extends SimpleApplication { } assetManager.registerLocator(storageFolder.getPath(), FileLocator.class); - model = assetManager.loadModel("newJaime.j3o"); - - rootNode.attachChild(model); + Spatial model2 = assetManager.loadModel("zophrac.j3o"); + model2.setLocalScale(0.1f); + probeNode.attachChild(model2); debugAppState = new ArmatureDebugAppState(); stateManager.attach(debugAppState); - setupModel(model); + setupModel(model2); flyCam.setEnabled(false); Node target = new Node("CamTarget"); //target.setLocalTransform(model.getLocalTransform()); - target.move(0, 1, 0); + target.move(0, 0, 0); ChaseCameraAppState chaseCam = new ChaseCameraAppState(); chaseCam.setTarget(target); getStateManager().attach(chaseCam); @@ -130,10 +128,14 @@ public class TestAnimSerialization extends SimpleApplication { } composer = model.getControl(AnimComposer.class); if (composer != null) { +// model.getControl(SkinningControl.class).setEnabled(false); +// model.getControl(MorphControl.class).setEnabled(false); +// composer.setEnabled(false); - SkinningControl sc = model.getControl(SkinningControl.class); + SkinningControl sc = model.getControl(SkinningControl.class); debugAppState.addArmatureFrom(sc); + anims.clear(); for (String name : composer.getAnimClipsNames()) { anims.add(name); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java index f6b85674a..f7a839327 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java @@ -52,7 +52,7 @@ public class TestArmature extends SimpleApplication { final Armature armature = new Armature(joints); //armature.setModelTransformClass(SeparateJointModelTransform.class); - armature.setBindPose(); + armature.saveBindPose(); //create animations AnimClip clip = new AnimClip("anim"); @@ -131,7 +131,7 @@ public class TestArmature extends SimpleApplication { public void onAction(String name, boolean isPressed, float tpf) { if (isPressed) { composer.reset(); - armature.resetToBindPose(); + armature.applyBindPose(); } else { composer.setCurrentAction("anim"); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java b/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java index 748bb43f3..24d8b21e5 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java @@ -59,7 +59,7 @@ public class TestBaseAnimSerialization extends SimpleApplication { armature = new Armature(joints); //armature.setModelTransformClass(SeparateJointModelTransform.class); - armature.setBindPose(); + armature.saveBindPose(); //create animations AnimClip clip = new AnimClip("anim"); @@ -153,7 +153,7 @@ public class TestBaseAnimSerialization extends SimpleApplication { public void onAction(String name, boolean isPressed, float tpf) { if (isPressed) { composer.reset(); - armature.resetToBindPose(); + armature.applyBindPose(); } else { composer.setCurrentAction("anim"); 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 fce2bf8ad..260e018e6 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 @@ -1009,6 +1009,7 @@ public class GltfLoader implements AssetLoader { skinnedSpatials.put(skinData, new ArrayList()); armature.update(); + armature.saveInitialPose(); } } diff --git a/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java b/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java index 88ab22853..d9977d3a9 100644 --- a/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java +++ b/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java @@ -161,7 +161,7 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { } indexToJoint.clear(); armature = new Armature(joints); - armature.setBindPose(); + armature.saveBindPose(); } else if (qName.equals("animation")) { animClips.add(animClip); animClip = null; From 8f4941f529d34f4aa0fbb8a55baa15661db324bf Mon Sep 17 00:00:00 2001 From: Nehon Date: Mon, 19 Mar 2018 20:15:00 +0100 Subject: [PATCH 30/54] Some clean up in PBR j3MD --- .../Common/MatDefs/Light/PBRLighting.j3md | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md index 2a0849439..a21ce6804 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md @@ -28,6 +28,9 @@ MaterialDef PBR Lighting { Texture2D RoughnessMap -LINEAR //Metallic and Roughness are packed respectively in the b and g channel of a single map + // r: unspecified + // g: Roughness + // b: Metallic Texture2D MetallicRoughnessMap -LINEAR // Texture of the emissive parts of the material @@ -47,17 +50,6 @@ MaterialDef PBR Lighting { Color Specular : 1.0 1.0 1.0 1.0 Float Glossiness : 1.0 - Vector4 ProbeData - - // Prefiltered Env Map for indirect specular lighting - TextureCubeMap PrefEnvMap -LINEAR - - // Irradiance map for indirect diffuse lighting - TextureCubeMap IrradianceMap -LINEAR - - //integrate BRDF map for indirect Lighting - Texture2D IntegrateBRDF -LINEAR - // Parallax/height map Texture2D ParallaxMap -LINEAR From f7ccdf486bd2ec6424247984624373c5448ecf72 Mon Sep 17 00:00:00 2001 From: Nehon Date: Sat, 24 Mar 2018 20:40:33 +0100 Subject: [PATCH 31/54] Removes the default value for NumberOfTargetsBuffers --- jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md | 2 +- .../src/main/resources/Common/MatDefs/Light/PBRLighting.j3md | 2 +- jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md b/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md index 2371b79a7..2a8847324 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md @@ -114,7 +114,7 @@ MaterialDef Phong Lighting { // For Morph animation FloatArray MorphWeights Int NumberOfMorphTargets - Int NumberOfTargetsBuffers : 1 + Int NumberOfTargetsBuffers //For instancing Boolean UseInstancing diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md index a21ce6804..437947d6c 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md @@ -106,7 +106,7 @@ MaterialDef PBR Lighting { // For Morph animation FloatArray MorphWeights Int NumberOfMorphTargets - Int NumberOfTargetsBuffers : 1 + Int NumberOfTargetsBuffers //For instancing Boolean UseInstancing diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md index b3e14ee9a..f765d2044 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md @@ -23,7 +23,7 @@ MaterialDef Unshaded { // For Morph animation FloatArray MorphWeights Int NumberOfMorphTargets - Int NumberOfTargetsBuffers : 1 + Int NumberOfTargetsBuffers // Alpha threshold for fragment discarding Float AlphaDiscardThreshold (AlphaTestFallOff) From 6b864f2d72f30f00dd52392e03f495a4d2b64513 Mon Sep 17 00:00:00 2001 From: Nehon Date: Sat, 24 Mar 2018 20:50:15 +0100 Subject: [PATCH 32/54] Remobes unused imports --- jme3-core/src/main/java/com/jme3/anim/MorphControl.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/anim/MorphControl.java b/jme3-core/src/main/java/com/jme3/anim/MorphControl.java index 80398ce6c..1aa055555 100644 --- a/jme3-core/src/main/java/com/jme3/anim/MorphControl.java +++ b/jme3-core/src/main/java/com/jme3/anim/MorphControl.java @@ -1,6 +1,6 @@ package com.jme3.anim; -import com.jme3.export.*; +import com.jme3.export.Savable; import com.jme3.material.*; import com.jme3.renderer.*; import com.jme3.scene.*; @@ -9,9 +9,7 @@ import com.jme3.scene.mesh.MorphTarget; import com.jme3.shader.VarType; import com.jme3.util.BufferUtils; import com.jme3.util.SafeArrayList; -import javafx.geometry.Pos; -import java.io.IOException; import java.nio.FloatBuffer; import java.util.logging.Level; import java.util.logging.Logger; From 7ded33051c9e3d49730b1d95c0f9f663cc8c1dbf Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sun, 25 Mar 2018 15:13:24 -0400 Subject: [PATCH 33/54] Modified AnimComposer.setCurrentAction() to return the Action. Makes it easier to set the speed and stuff. --- jme3-core/src/main/java/com/jme3/anim/AnimComposer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index 6a8f92304..73bdd4b4b 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -59,9 +59,10 @@ public class AnimComposer extends AbstractControl { animClipMap.remove(anim.getName()); } - public void setCurrentAction(String name) { + public Action setCurrentAction(String name) { currentAction = action(name); time = 0; + return currentAction; } public Action action(String name) { From a463f5515a1a8566977cc57b5a97482102e0098b Mon Sep 17 00:00:00 2001 From: Nehon Date: Mon, 26 Mar 2018 06:29:20 +0200 Subject: [PATCH 34/54] Negative speed now plays an animation backwards --- .../main/java/com/jme3/anim/tween/action/Action.java | 5 ++++- .../java/jme3test/model/anim/TestAnimMigration.java | 10 ++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java index 21d5bdc62..25379bf92 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java @@ -22,7 +22,10 @@ public abstract class Action implements Tween { @Override public boolean interpolate(double t) { - return subInterpolate(t * speed); + t = t * speed; + // make sure negative time is in [0, length] range + t = (t % length + length) % length; + return subInterpolate(t); } public abstract boolean subInterpolate(double t); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index 9a3782676..cc90a6053 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -165,20 +165,14 @@ public class TestAnimMigration extends SimpleApplication { composer.actionSequence("Sequence", composer.makeAction("Walk"), composer.makeAction("Run"), - composer.makeAction("Jumping")).setSpeed(4); + composer.makeAction("Jumping")).setSpeed(2); action = composer.actionBlended("Blend", new LinearBlendSpace(1, 4), "Walk", "Run"); action.getBlendSpace().setValue(1); - composer.action("Walk").setSpeed(2); - -// composer.actionSequence("Sequence", -// composer.tweenFromClip("Walk"), -// composer.tweenFromClip("Dodge"), -// composer.tweenFromClip("push")); - + composer.action("Walk").setSpeed(-1); anims.addFirst("Sequence"); anims.addFirst("Blend"); From 06f8a00549b56a357f55e2291ba3b123e8dc53a1 Mon Sep 17 00:00:00 2001 From: Stephen Gold Date: Fri, 23 Mar 2018 01:42:08 -0700 Subject: [PATCH 35/54] fix JME issue #742 (attachment nodes for ignoreTransform geometries) --- .../main/java/com/jme3/animation/Bone.java | 10 +++- .../src/main/java/com/jme3/scene/Spatial.java | 47 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/animation/Bone.java b/jme3-core/src/main/java/com/jme3/animation/Bone.java index 6cd751e3c..b60273ecc 100644 --- a/jme3-core/src/main/java/com/jme3/animation/Bone.java +++ b/jme3-core/src/main/java/com/jme3/animation/Bone.java @@ -535,11 +535,19 @@ public final class Bone implements Savable, JmeCloneable { attachNode.setLocalRotation(modelRot); attachNode.setLocalScale(modelScale); + } else if (targetGeometry.isIgnoreTransform()) { + /* + * The animated meshes ignore transforms: match the world transform + * of the attachments node to the bone's transform. + */ + Transform combined = new Transform(modelPos, modelRot, modelScale); + attachNode.setWorldTransform(combined); + } else { Spatial loopSpatial = targetGeometry; Transform combined = new Transform(modelPos, modelRot, modelScale); /* - * Climb the scene graph applying local transforms until the + * Climb the scene graph applying local transforms until the * attachments node's parent is reached. */ while (loopSpatial != attachParent && loopSpatial != null) { diff --git a/jme3-core/src/main/java/com/jme3/scene/Spatial.java b/jme3-core/src/main/java/com/jme3/scene/Spatial.java index c44db734b..ec2d992f7 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java +++ b/jme3-core/src/main/java/com/jme3/scene/Spatial.java @@ -495,6 +495,53 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab return worldTransform; } + /** + * Alter the local transform so that the world transform approximates the + * specified value. + * + * @param world desired world transform (not null, unaffected) + * @throws IllegalArgumentException if the spatial ignores transform OR the + * parent's world transform isn't invertible + */ + public void setWorldTransform(Transform world) { + if (this instanceof Geometry && ((Geometry) this).ignoreTransform) { + throw new RuntimeException("spatial ignores transforms"); + } + + if (parent == null) { + /* + * special case: for a root spatial, the world transform is + * precisely the local transform + */ + setLocalTransform(world); + return; + } + + Transform parentTransform = parent.getWorldTransform(); + Vector3f parentScale = parentTransform.getScale(); + if (parentScale.x == 0f || parentScale.y == 0f || parentScale.z == 0f) { + throw new RuntimeException("parent scale isn't invertible"); + } + Quaternion parentInvRotation = parentTransform.getRotation().inverse(); + if (parentInvRotation == null) { + throw new RuntimeException("parent rotation isn't invertible"); + } + /* + * Undo the operations of Transform.combineWithParent() + */ + Transform tmpLocal = world.clone(); + Vector3f translation = tmpLocal.getTranslation(); + Quaternion rotation = tmpLocal.getRotation(); + tmpLocal.getScale().divideLocal(parentScale); + parentInvRotation.mult(rotation, rotation); + Vector3f parentTranslation = parentTransform.getTranslation(); + translation.subtractLocal(parentTranslation); + parentInvRotation.multLocal(translation); + translation.divideLocal(parentScale); + + setLocalTransform(tmpLocal); + } + /** * rotateUpTo is a utility function that alters the * local rotation to point the Y axis in the direction given by newUp. From 9df4449f49d39c9a0287c49f073b7e8cce66b862 Mon Sep 17 00:00:00 2001 From: Nehon Date: Mon, 2 Apr 2018 09:08:16 +0200 Subject: [PATCH 36/54] Adds animation mask to the animation system --- .../src/main/java/com/jme3/anim/AnimClip.java | 5 ++ .../main/java/com/jme3/anim/AnimComposer.java | 78 ++++++++++++++++--- .../java/com/jme3/anim/AnimationMask.java | 12 +++ .../src/main/java/com/jme3/anim/Armature.java | 3 + .../main/java/com/jme3/anim/ArmatureMask.java | 62 +++++++++++++++ .../src/main/java/com/jme3/anim/Joint.java | 9 +++ .../jme3/anim/MatrixJointModelTransform.java | 5 +- .../com/jme3/anim/tween/action/Action.java | 10 +++ .../jme3/anim/tween/action/ClipAction.java | 10 ++- .../model/anim/TestAnimMigration.java | 16 +++- 10 files changed, 195 insertions(+), 15 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/anim/AnimationMask.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java index 7f8c022c5..9b86858a3 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java @@ -66,6 +66,11 @@ public class AnimClip implements JmeCloneable, Savable { this.tracks = newTracks; } + @Override + public String toString() { + return "Clip " + name + ", " + length + 's'; + } + @Override public void write(JmeExporter ex) throws IOException { OutputCapsule oc = ex.getCapsule(this); diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index 73bdd4b4b..4b115037b 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -8,6 +8,7 @@ import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.control.AbstractControl; import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; import java.io.IOException; import java.util.*; @@ -17,12 +18,16 @@ import java.util.*; */ public class AnimComposer extends AbstractControl { + public static final String DEFAULT_LAYER = "Default"; private Map animClipMap = new HashMap<>(); - private Action currentAction; private Map actions = new HashMap<>(); private float globalSpeed = 1f; - private float time; + private Map layers = new LinkedHashMap<>(); + + public AnimComposer() { + layers.put(DEFAULT_LAYER, new Layer()); + } /** * Retrieve an animation from the list of animations. @@ -60,8 +65,17 @@ public class AnimComposer extends AbstractControl { } public Action setCurrentAction(String name) { - currentAction = action(name); - time = 0; + return setCurrentAction(name, DEFAULT_LAYER); + } + + public Action setCurrentAction(String actionName, String layerName) { + Layer l = layers.get(layerName); + if (l == null) { + throw new IllegalArgumentException("Unknown layer " + layerName); + } + Action currentAction = action(actionName); + l.time = 0; + l.currentAction = currentAction; return currentAction; } @@ -84,6 +98,12 @@ public class AnimComposer extends AbstractControl { return action; } + public void makeLayer(String name, AnimationMask mask){ + Layer l = new Layer(); + l.mask = mask; + layers.put(name, l); + } + public BaseAction actionSequence(String name, Tween... tweens) { BaseAction action = new BaseAction(Tweens.sequence(tweens)); @@ -103,8 +123,10 @@ public class AnimComposer extends AbstractControl { } public void reset() { - currentAction = null; - time = 0; + for (Layer layer : layers.values()) { + layer.currentAction = null; + layer.time = 0; + } } public Collection getAnimClips() { @@ -117,11 +139,17 @@ public class AnimComposer extends AbstractControl { @Override protected void controlUpdate(float tpf) { - if (currentAction != null) { - time += tpf; - boolean running = currentAction.interpolate(time * globalSpeed); + for (Layer layer : layers.values()) { + Action currentAction = layer.currentAction; + if (currentAction == null) { + continue; + } + layer.time += tpf; + currentAction.setMask(layer.mask); + boolean running = currentAction.interpolate(layer.time * globalSpeed); + currentAction.setMask(null); if (!running) { - time = 0; + layer.time = 0; } } } @@ -162,6 +190,14 @@ public class AnimComposer extends AbstractControl { } actions = act; animClipMap = clips; + + Map newLayers = new LinkedHashMap<>(); + for (String key : layers.keySet()) { + newLayers.put(key, cloner.clone(layers.get(key))); + } + + layers = newLayers; + } @Override @@ -177,4 +213,26 @@ public class AnimComposer extends AbstractControl { OutputCapsule oc = ex.getCapsule(this); oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap()); } + + public static class Layer implements JmeCloneable { + private Action currentAction; + private AnimationMask mask; + private float weight; + private float time; + + @Override + public Object jmeClone() { + try { + Layer clone = (Layer) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + currentAction = null; + } + } } diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimationMask.java b/jme3-core/src/main/java/com/jme3/anim/AnimationMask.java new file mode 100644 index 000000000..2aa8328b6 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/AnimationMask.java @@ -0,0 +1,12 @@ +package com.jme3.anim; + +/** + * Created by Nehon + * An AnimationMask is defining a subset of elements on which an animation will be applied. + * Most used implementation is the ArmatureMask that defines a subset of joints in an Armature. + */ +public interface AnimationMask { + + boolean contains(Object target); + +} \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/anim/Armature.java b/jme3-core/src/main/java/com/jme3/anim/Armature.java index 39a664ede..ad7f5b0ac 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Armature.java +++ b/jme3-core/src/main/java/com/jme3/anim/Armature.java @@ -46,6 +46,7 @@ public class Armature implements JmeCloneable, Savable { List rootJointList = new ArrayList<>(); for (int i = jointList.length - 1; i >= 0; i--) { Joint joint = jointList[i]; + joint.setId(i); instanciateJointModelTransform(joint); if (joint.getParent() == null) { rootJointList.add(joint); @@ -276,7 +277,9 @@ public class Armature implements JmeCloneable, Savable { throw new AssetLoadException("Cannnot find class for name " + className); } + int i = 0; for (Joint joint : jointList) { + joint.setId(i++); instanciateJointModelTransform(joint); } createSkinningMatrices(); diff --git a/jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java b/jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java new file mode 100644 index 000000000..b492eb4ed --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java @@ -0,0 +1,62 @@ +package com.jme3.anim; + +import java.util.BitSet; + +public class ArmatureMask implements AnimationMask { + + private BitSet affectedJoints = new BitSet(); + + @Override + public boolean contains(Object target) { + return affectedJoints.get(((Joint) target).getId()); + } + + public static ArmatureMask createMask(Armature armature, String fromJoint) { + ArmatureMask mask = new ArmatureMask(); + mask.addFromJoint(armature, fromJoint); + return mask; + } + + public static ArmatureMask createMask(Armature armature, String... joints) { + ArmatureMask mask = new ArmatureMask(); + mask.addBones(armature, joints); + for (String joint : joints) { + mask.affectedJoints.set(armature.getJoint(joint).getId()); + } + return mask; + } + + /** + * Add joints to be influenced by this animation mask. + */ + public void addBones(Armature armature, String... jointNames) { + for (String jointName : jointNames) { + Joint joint = findJoint(armature, jointName); + affectedJoints.set(joint.getId()); + } + } + + private Joint findJoint(Armature armature, String jointName) { + Joint joint = armature.getJoint(jointName); + if (joint == null) { + throw new IllegalArgumentException("Cannot find joint " + jointName); + } + return joint; + } + + /** + * Add a joint and all its sub armature joints to be influenced by this animation mask. + */ + public void addFromJoint(Armature armature, String jointName) { + Joint joint = findJoint(armature, jointName); + recurseAddJoint(joint); + } + + private void recurseAddJoint(Joint joint) { + affectedJoints.set(joint.getId()); + for (Joint j : joint.getChildren()) { + recurseAddJoint(j); + } + } + +} diff --git a/jme3-core/src/main/java/com/jme3/anim/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java index f4c0a0d1b..bf672fa29 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Joint.java +++ b/jme3-core/src/main/java/com/jme3/anim/Joint.java @@ -22,6 +22,7 @@ import java.util.List; public class Joint implements Savable, JmeCloneable, HasLocalTransform { private String name; + private int id; private Joint parent; private SafeArrayList children = new SafeArrayList<>(Joint.class); private Geometry targetGeometry; @@ -280,6 +281,14 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform { return inverseModelBindMatrix; } + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + @Override public Object jmeClone() { try { diff --git a/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java b/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java index 27964f325..5f1aadad7 100644 --- a/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java +++ b/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java @@ -19,11 +19,11 @@ public class MatrixJointModelTransform implements JointModelTransform { if (parent != null) { ((MatrixJointModelTransform) parent.getJointModelTransform()).getModelTransformMatrix().mult(modelTransformMatrix, modelTransformMatrix); } - modelTransform.fromTransformMatrix(modelTransformMatrix); + } public void getOffsetTransform(Matrix4f outTransform, Matrix4f inverseModelBindMatrix) { - outTransform.set(modelTransformMatrix).mult(inverseModelBindMatrix, outTransform); + modelTransformMatrix.mult(inverseModelBindMatrix, outTransform); } @Override @@ -41,6 +41,7 @@ public class MatrixJointModelTransform implements JointModelTransform { @Override public Transform getModelTransform() { + modelTransform.fromTransformMatrix(modelTransformMatrix); return modelTransform; } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java index 25379bf92..85bd9ae01 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java @@ -1,5 +1,6 @@ package com.jme3.anim.tween.action; +import com.jme3.anim.AnimationMask; import com.jme3.anim.tween.Tween; public abstract class Action implements Tween { @@ -7,6 +8,7 @@ public abstract class Action implements Tween { protected Action[] actions; private double length; private double speed = 1; + private AnimationMask mask; protected Action(Tween... tweens) { this.actions = new Action[tweens.length]; @@ -46,4 +48,12 @@ public abstract class Action implements Tween { public void setSpeed(double speed) { this.speed = speed; } + + public AnimationMask getMask() { + return mask; + } + + public void setMask(AnimationMask mask) { + this.mask = mask; + } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java index 59d24261f..9f0d49f20 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java @@ -25,7 +25,11 @@ public class ClipAction extends BlendableAction { AnimTrack[] tracks = clip.getTracks(); for (AnimTrack track : tracks) { if (track instanceof TransformTrack) { - interpolateTransformTrack(t, (TransformTrack) track); + TransformTrack tt = (TransformTrack) track; + if(getMask() != null && !getMask().contains(tt.getTarget())){ + continue; + } + interpolateTransformTrack(t, tt); } else if (track instanceof MorphTrack) { interpolateMorphTrack(t, (MorphTrack) track); } @@ -60,6 +64,10 @@ public class ClipAction extends BlendableAction { } + public String toString() { + return clip.toString(); + } + @Override public Collection getTargets() { List targets = new ArrayList<>(clip.getTracks().length); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index cc90a6053..df19c652e 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -1,7 +1,6 @@ package jme3test.model.anim; -import com.jme3.anim.AnimComposer; -import com.jme3.anim.SkinningControl; +import com.jme3.anim.*; import com.jme3.anim.tween.action.Action; import com.jme3.anim.tween.action.BlendAction; import com.jme3.anim.tween.action.BlendableAction; @@ -125,8 +124,19 @@ public class TestAnimMigration extends SimpleApplication { } }, "toggleArmature"); + inputManager.addMapping("mask", new KeyTrigger(KeyInput.KEY_M)); + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed) { + composer.setCurrentAction("Wave", "LeftArm"); + } + } + }, "mask"); + inputManager.addMapping("blendUp", new KeyTrigger(KeyInput.KEY_UP)); inputManager.addMapping("blendDown", new KeyTrigger(KeyInput.KEY_DOWN)); + inputManager.addListener(new AnalogListener() { @Override @@ -174,6 +184,8 @@ public class TestAnimMigration extends SimpleApplication { composer.action("Walk").setSpeed(-1); + composer.makeLayer("LeftArm", ArmatureMask.createMask(sc.getArmature(), "shoulder.L")); + anims.addFirst("Sequence"); anims.addFirst("Blend"); From 824e99c96e6e183d81f13fbaef00db2cf8252c14 Mon Sep 17 00:00:00 2001 From: Nehon Date: Mon, 2 Apr 2018 09:36:29 +0200 Subject: [PATCH 37/54] Fixes attachement node when model ignore transforms --- .../main/java/com/jme3/animation/Bone.java | 6 ++- .../src/main/java/com/jme3/scene/Spatial.java | 47 ------------------- 2 files changed, 4 insertions(+), 49 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/animation/Bone.java b/jme3-core/src/main/java/com/jme3/animation/Bone.java index b60273ecc..edc1b8894 100644 --- a/jme3-core/src/main/java/com/jme3/animation/Bone.java +++ b/jme3-core/src/main/java/com/jme3/animation/Bone.java @@ -540,8 +540,10 @@ public final class Bone implements Savable, JmeCloneable { * The animated meshes ignore transforms: match the world transform * of the attachments node to the bone's transform. */ - Transform combined = new Transform(modelPos, modelRot, modelScale); - attachNode.setWorldTransform(combined); + attachNode.setLocalTranslation(modelPos); + attachNode.setLocalRotation(modelRot); + attachNode.setLocalScale(modelScale); + attachNode.getLocalTransform().combineWithParent(attachNode.getParent().getWorldTransform().invert()); } else { Spatial loopSpatial = targetGeometry; diff --git a/jme3-core/src/main/java/com/jme3/scene/Spatial.java b/jme3-core/src/main/java/com/jme3/scene/Spatial.java index ec2d992f7..c44db734b 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java +++ b/jme3-core/src/main/java/com/jme3/scene/Spatial.java @@ -495,53 +495,6 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab return worldTransform; } - /** - * Alter the local transform so that the world transform approximates the - * specified value. - * - * @param world desired world transform (not null, unaffected) - * @throws IllegalArgumentException if the spatial ignores transform OR the - * parent's world transform isn't invertible - */ - public void setWorldTransform(Transform world) { - if (this instanceof Geometry && ((Geometry) this).ignoreTransform) { - throw new RuntimeException("spatial ignores transforms"); - } - - if (parent == null) { - /* - * special case: for a root spatial, the world transform is - * precisely the local transform - */ - setLocalTransform(world); - return; - } - - Transform parentTransform = parent.getWorldTransform(); - Vector3f parentScale = parentTransform.getScale(); - if (parentScale.x == 0f || parentScale.y == 0f || parentScale.z == 0f) { - throw new RuntimeException("parent scale isn't invertible"); - } - Quaternion parentInvRotation = parentTransform.getRotation().inverse(); - if (parentInvRotation == null) { - throw new RuntimeException("parent rotation isn't invertible"); - } - /* - * Undo the operations of Transform.combineWithParent() - */ - Transform tmpLocal = world.clone(); - Vector3f translation = tmpLocal.getTranslation(); - Quaternion rotation = tmpLocal.getRotation(); - tmpLocal.getScale().divideLocal(parentScale); - parentInvRotation.mult(rotation, rotation); - Vector3f parentTranslation = parentTransform.getTranslation(); - translation.subtractLocal(parentTranslation); - parentInvRotation.multLocal(translation); - translation.divideLocal(parentScale); - - setLocalTransform(tmpLocal); - } - /** * rotateUpTo is a utility function that alters the * local rotation to point the Y axis in the direction given by newUp. From 83aef82d927da14ea70018c2a78dd9b7126348d1 Mon Sep 17 00:00:00 2001 From: Nehon Date: Tue, 17 Apr 2018 15:23:56 +0200 Subject: [PATCH 38/54] Adds support for light porbe blending and Oriented box light probes with corrected parallax --- .../java/com/jme3/bounding/Intersection.java | 10 + .../jme3/environment/EnvironmentCamera.java | 13 +- .../jme3/environment/LightProbeFactory.java | 21 ++ .../environment/util/LightsDebugState.java | 30 +- .../com/jme3/light/DefaultLightFilter.java | 10 +- .../main/java/com/jme3/light/LightProbe.java | 177 ++++++---- .../light/LightProbeBlendingProcessor.java | 214 ------------- .../com/jme3/light/OrientedBoxProbeArea.java | 249 +++++++++++++++ .../jme3/light/PoiLightProbeLightFilter.java | 107 ------- .../main/java/com/jme3/light/PointLight.java | 7 +- .../main/java/com/jme3/light/ProbeArea.java | 33 ++ .../java/com/jme3/light/SphereProbeArea.java | 102 ++++++ .../light/WeightedProbeBlendingStrategy.java | 77 +++++ .../SinglePassAndImageBasedLightingLogic.java | 72 +++-- .../com/jme3/scene/debug/WireFrustum.java | 42 ++- .../Common/MatDefs/Light/PBRLighting.frag | 159 +++++++-- .../Common/MatDefs/Light/PBRLighting.j3md | 4 + .../main/java/jme3test/collision/Main.java | 0 .../main/java/jme3test/light/DlsfError.java | 0 .../jme3test/light/TestConeVSFrustum.java | 2 +- .../java/jme3test/light/TestObbVsBounds.java | 301 ++++++++++++++++++ .../main/java/jme3test/light/pbr/RefEnv.java | 4 +- .../jme3test/light/pbr/TestPBRLighting.java | 5 +- 23 files changed, 1153 insertions(+), 486 deletions(-) delete mode 100644 jme3-core/src/main/java/com/jme3/light/LightProbeBlendingProcessor.java create mode 100644 jme3-core/src/main/java/com/jme3/light/OrientedBoxProbeArea.java delete mode 100644 jme3-core/src/main/java/com/jme3/light/PoiLightProbeLightFilter.java create mode 100644 jme3-core/src/main/java/com/jme3/light/ProbeArea.java create mode 100644 jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java create mode 100644 jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java create mode 100644 jme3-examples/src/main/java/jme3test/collision/Main.java create mode 100644 jme3-examples/src/main/java/jme3test/light/DlsfError.java create mode 100644 jme3-examples/src/main/java/jme3test/light/TestObbVsBounds.java 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 c627e23e5..37beede01 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/Intersection.java +++ b/jme3-core/src/main/java/com/jme3/bounding/Intersection.java @@ -34,6 +34,7 @@ package com.jme3.bounding; import com.jme3.math.FastMath; import com.jme3.math.Plane; import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; import com.jme3.util.TempVars; import static java.lang.Math.max; import static java.lang.Math.min; @@ -107,6 +108,15 @@ public final class Intersection { } } + public static boolean intersect(Camera camera, Vector3f center,float radius){ + for (int i = 5; i >= 0; i--) { + if (camera.getWorldPlane(i).pseudoDistance(center) <= -radius) { + return false; + } + } + return true; + } + // private boolean axisTest(float a, float b, float fa, float fb, Vector3f v0, Vector3f v1, ) // private boolean axisTestX01(float a, float b, float fa, float fb, // Vector3f center, Vector3f ext, diff --git a/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java b/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java index aff4dd0b9..a8fa7de37 100644 --- a/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java +++ b/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java @@ -38,14 +38,9 @@ import com.jme3.environment.util.EnvMapUtils; import com.jme3.light.LightProbe; import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; -import com.jme3.renderer.Camera; -import com.jme3.renderer.RenderManager; -import com.jme3.renderer.ViewPort; +import com.jme3.renderer.*; import com.jme3.scene.Spatial; -import com.jme3.texture.FrameBuffer; -import com.jme3.texture.Image; -import com.jme3.texture.Texture2D; -import com.jme3.texture.TextureCubeMap; +import com.jme3.texture.*; import com.jme3.texture.image.ColorSpace; import com.jme3.util.BufferUtils; import com.jme3.util.MipMapGenerator; @@ -119,7 +114,7 @@ public class EnvironmentCamera extends BaseAppState { private final List jobs = new ArrayList(); /** - * Creates an EnvironmentCamera with a size of 128 + * Creates an EnvironmentCamera with a size of 256 */ public EnvironmentCamera() { } @@ -322,7 +317,7 @@ public class EnvironmentCamera extends BaseAppState { final Camera offCamera = new Camera(mapSize, mapSize); offCamera.setLocation(worldPos); offCamera.setAxes(axisX, axisY, axisZ); - offCamera.setFrustumPerspective(90f, 1f, 1, 1000); + offCamera.setFrustumPerspective(90f, 1f, 0.1f, 1000); offCamera.setLocation(position); return offCamera; } diff --git a/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java b/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java index 9a4259804..859472fe1 100644 --- a/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java +++ b/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java @@ -32,6 +32,7 @@ package com.jme3.environment; import com.jme3.app.Application; +import com.jme3.asset.AssetManager; import com.jme3.environment.generation.*; import com.jme3.environment.util.EnvMapUtils; import com.jme3.light.LightProbe; @@ -204,6 +205,26 @@ public class LightProbeFactory { } } + /** + * For debuging porpose only + * Will return a Node meant to be added to a GUI presenting the 2 cube maps in a cross pattern with all the mip maps. + * + * @param manager the asset manager + * @return a debug node + */ + public static Node getDebugGui(AssetManager manager, LightProbe probe) { + if (!probe.isReady()) { + throw new UnsupportedOperationException("This EnvProbe is not ready yet, try to test isReady()"); + } + + Node debugNode = new Node("debug gui probe"); + Node debugPfemCm = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(probe.getPrefilteredEnvMap(), manager); + debugNode.attachChild(debugPfemCm); + debugPfemCm.setLocalTranslation(520, 0, 0); + + return debugNode; + } + /** * An inner class to keep the state of a generation process */ diff --git a/jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java b/jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java index 3d41148c4..a9d2a9e46 100644 --- a/jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java +++ b/jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java @@ -34,9 +34,8 @@ package com.jme3.environment.util; import com.jme3.app.Application; import com.jme3.app.state.BaseAppState; import com.jme3.bounding.BoundingSphere; +import com.jme3.light.*; import com.jme3.material.Material; -import com.jme3.light.LightProbe; -import com.jme3.light.Light; import com.jme3.renderer.RenderManager; import com.jme3.scene.Geometry; import com.jme3.scene.Node; @@ -68,7 +67,7 @@ public class LightsDebugState extends BaseAppState { @Override protected void initialize(Application app) { debugNode = new Node("Environment debug Node"); - Sphere s = new Sphere(16, 16, 1); + Sphere s = new Sphere(16, 16, 0.15f); debugGeom = new Geometry("debugEnvProbe", s); debugMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/reflect.j3md"); debugGeom.setMaterial(debugMaterial); @@ -80,6 +79,16 @@ public class LightsDebugState extends BaseAppState { @Override public void update(float tpf) { + if(!isEnabled()){ + return; + } + updateLights(scene); + debugNode.updateLogicalState(tpf); + debugNode.updateGeometricState(); + cleanProbes(); + } + + public void updateLights(Spatial scene) { for (Light light : scene.getWorldLightList()) { switch (light.getType()) { @@ -101,16 +110,18 @@ public class LightsDebugState extends BaseAppState { m.setTexture("CubeMap", probe.getPrefilteredEnvMap()); } n.setLocalTranslation(probe.getPosition()); - n.getChild(1).setLocalScale(((BoundingSphere) probe.getBounds()).getRadius()); + n.getChild(1).setLocalScale(probe.getArea().getRadius()); break; default: break; } } - debugNode.updateLogicalState(tpf); - debugNode.updateGeometricState(); - cleanProbes(); - + if( scene instanceof Node){ + Node n = (Node)scene; + for (Spatial spatial : n.getChildren()) { + updateLights(spatial); + } + } } /** @@ -138,6 +149,9 @@ public class LightsDebugState extends BaseAppState { @Override public void render(RenderManager rm) { + if(!isEnabled()){ + return; + } rm.renderScene(debugNode, getApplication().getViewPort()); } 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 84cdc17fc..df735095e 100644 --- a/jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java +++ b/jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java @@ -43,10 +43,10 @@ public final class DefaultLightFilter implements LightFilter { private Camera camera; private final HashSet processedLights = new HashSet(); - private final LightProbeBlendingStrategy probeBlendStrat; + private LightProbeBlendingStrategy probeBlendStrat; public DefaultLightFilter() { - probeBlendStrat = new BasicProbeBlendingStrategy(); + probeBlendStrat = new WeightedProbeBlendingStrategy(); } public DefaultLightFilter(LightProbeBlendingStrategy probeBlendStrat) { @@ -113,5 +113,9 @@ public final class DefaultLightFilter implements LightFilter { vars.release(); } } - + + public void setLightProbeBlendingStrategy(LightProbeBlendingStrategy strategy){ + probeBlendStrat = strategy; + } + } diff --git a/jme3-core/src/main/java/com/jme3/light/LightProbe.java b/jme3-core/src/main/java/com/jme3/light/LightProbe.java index 2b962fc97..d6d716877 100644 --- a/jme3-core/src/main/java/com/jme3/light/LightProbe.java +++ b/jme3-core/src/main/java/com/jme3/light/LightProbe.java @@ -31,24 +31,16 @@ */ package com.jme3.light; -import com.jme3.asset.AssetManager; -import com.jme3.bounding.BoundingBox; -import com.jme3.bounding.BoundingSphere; -import com.jme3.bounding.BoundingVolume; +import com.jme3.bounding.*; import com.jme3.environment.EnvironmentCamera; import com.jme3.environment.LightProbeFactory; -import com.jme3.environment.util.EnvMapUtils; -import com.jme3.export.InputCapsule; -import com.jme3.export.JmeExporter; -import com.jme3.export.JmeImporter; -import com.jme3.export.OutputCapsule; -import com.jme3.export.Savable; -import com.jme3.math.Vector3f; +import com.jme3.export.*; +import com.jme3.math.*; import com.jme3.renderer.Camera; -import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.texture.TextureCubeMap; import com.jme3.util.TempVars; + import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -56,7 +48,7 @@ import java.util.logging.Logger; /** * A LightProbe is not exactly a light. It holds environment map information used for Image Based Lighting. * This is used for indirect lighting in the Physically Based Rendering pipeline. - * + * * A light probe has a position in world space. This is the position from where the Environment Map are rendered. * There are two environment data structure held by the LightProbe : * - The irradiance spherical harmonics factors (used for indirect diffuse lighting in the PBR pipeline). @@ -64,10 +56,10 @@ import java.util.logging.Logger; * Note that when instantiating the LightProbe, both of those structures are null. * To compute them see {@link LightProbeFactory#makeProbe(com.jme3.environment.EnvironmentCamera, com.jme3.scene.Node)} * and {@link EnvironmentCamera}. - * - * The light probe has an area of effect that is a bounding volume centered on its position. (for now only Bounding spheres are supported). - * - * A LightProbe will only be taken into account when it's marked as ready. + * + * The light probe has an area of effect centered on its position. It can have a Spherical area or an Oriented Box area + * + * A LightProbe will only be taken into account when it's marked as ready and enabled. * A light probe is ready when it has valid environment map data set. * Note that you should never call setReady yourself. * @@ -78,20 +70,25 @@ import java.util.logging.Logger; public class LightProbe extends Light implements Savable { private static final Logger logger = Logger.getLogger(LightProbe.class.getName()); + public static final Matrix4f FALLBACK_MATRIX = new Matrix4f(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1); private Vector3f[] shCoeffs; private TextureCubeMap prefilteredEnvMap; - private BoundingVolume bounds = new BoundingSphere(1.0f, Vector3f.ZERO); + private ProbeArea area = new SphereProbeArea(Vector3f.ZERO, 1.0f); private boolean ready = false; private Vector3f position = new Vector3f(); - private Node debugNode; private int nbMipMaps; + public enum AreaType{ + Spherical, + OrientedBox + } + /** - * Empty constructor used for serialization. + * Empty constructor used for serialization. * You should never call it, use {@link LightProbeFactory#makeProbe(com.jme3.environment.EnvironmentCamera, com.jme3.scene.Node)} instead */ - public LightProbe() { + public LightProbe() { } /** @@ -104,13 +101,59 @@ public class LightProbe extends Light implements Savable { } /** - * Sets the prefiltered environment map - * @param prefileteredEnvMap the prefiltered environment map + * Sets the prefiltered environment map + * @param prefileteredEnvMap the prefiltered environment map */ public void setPrefilteredMap(TextureCubeMap prefileteredEnvMap) { this.prefilteredEnvMap = prefileteredEnvMap; } + /** + * Returns the data to send to the shader. + * This is a column major matrix that is not a classic transform matrix, it's laid out in a particular way + // 3x3 rot mat| + // 0 1 2 | 3 + // 0 | ax bx cx | px | ) + // 1 | ay by cy | py | probe position + // 2 | az bz cz | pz | ) + // --|----------| + // 3 | sx sy sz sp | -> 1/probe radius + nbMipMaps + // --scale-- + *

+ * (ax, ay, az) is the pitch rotation axis + * (bx, by, bz) is the yaw rotation axis + * (cx, cy, cz) is the roll rotation axis + * Like in a standard 3x3 rotation matrix. + * It's also the valid rotation matrix of the probe in world space. + * Note that for the Spherical Probe area this part is a 3x3 identity matrix. + *

+ * (px, py, pz) is the position of the center of the probe in world space + * Like in a valid 4x4 transform matrix. + *

+ * (sx, sy, sy) is the extent of the probe ( the scale ) + * In a standard transform matrix the scale is applied to the rotation matrix part. + * In the shader we need the rotation and the scale to be separated, doing this avoid to extract + * the scale from a classic transform matrix in the shader + *

+ * (sp) is a special entry, it contains the packed number of mip maps of the probe and the inverse radius for the probe. + * since the inverse radius in lower than 1, it's packed in the decimal part of the float. + * The number of mip maps is packed in the integer part of the float. + * (ie: for 6 mip maps and a radius of 3, sp= 6.3333333) + *

+ * The radius is obvious for a SphereProbeArea, + * but in the case of a OrientedBoxProbeArea it's the max of the extent vector's components. + */ + public Matrix4f getUniformMatrix(){ + + Matrix4f mat = area.getUniformMatrix(); + + // setting the (sp) entry of the matrix + mat.m33 = nbMipMaps + 1f / area.getRadius(); + + return mat; + } + + @Override public void write(JmeExporter ex) throws IOException { super.write(ex); @@ -118,7 +161,7 @@ public class LightProbe extends Light implements Savable { oc.write(shCoeffs, "shCoeffs", null); oc.write(prefilteredEnvMap, "prefilteredEnvMap", null); oc.write(position, "position", null); - oc.write(bounds, "bounds", new BoundingSphere(1.0f, Vector3f.ZERO)); + oc.write(area, "area", new SphereProbeArea(Vector3f.ZERO, 1.0f)); oc.write(ready, "ready", false); oc.write(nbMipMaps, "nbMipMaps", 0); } @@ -127,10 +170,16 @@ public class LightProbe extends Light implements Savable { public void read(JmeImporter im) throws IOException { super.read(im); InputCapsule ic = im.getCapsule(this); - + prefilteredEnvMap = (TextureCubeMap) ic.readSavable("prefilteredEnvMap", null); position = (Vector3f) ic.readSavable("position", null); - bounds = (BoundingVolume) ic.readSavable("bounds", new BoundingSphere(1.0f, Vector3f.ZERO)); + area = (ProbeArea)ic.readSavable("area", null); + if(area == null) { + // retro compat + BoundingSphere bounds = (BoundingSphere) ic.readSavable("bounds", new BoundingSphere(1.0f, Vector3f.ZERO)); + area = new SphereProbeArea(bounds.getCenter(), bounds.getRadius()); + } + area.setCenter(position); nbMipMaps = ic.readInt("nbMipMaps", 0); ready = ic.readBoolean("ready", false); @@ -146,25 +195,49 @@ public class LightProbe extends Light implements Savable { } } + /** * returns the bounding volume of this LightProbe * @return a bounding volume. + * @deprecated use {@link LightProbe#getArea()} */ + @Deprecated public BoundingVolume getBounds() { - return bounds; + return new BoundingSphere(((SphereProbeArea)area).getRadius(), ((SphereProbeArea)area).getCenter()); } - + /** * Sets the bounds of this LightProbe - * Note that for now only BoundingSphere is supported and this method will + * Note that for now only BoundingSphere is supported and this method will * throw an UnsupportedOperationException with any other BoundingVolume type * @param bounds the bounds of the LightProbe + * @deprecated */ + @Deprecated public void setBounds(BoundingVolume bounds) { - if( bounds.getType()!= BoundingVolume.Type.Sphere){ - throw new UnsupportedOperationException("For not only BoundingSphere are suported for LightProbe"); + } + + public ProbeArea getArea() { + return area; + } + + public void setAreaType(AreaType type){ + switch (type){ + case Spherical: + area = new SphereProbeArea(Vector3f.ZERO, 1.0f); + break; + case OrientedBox: + area = new OrientedBoxProbeArea(new Transform()); + area.setCenter(position); + break; + } + } + + public AreaType getAreaType(){ + if(area instanceof SphereProbeArea){ + return AreaType.Spherical; } - this.bounds = bounds; + return AreaType.OrientedBox; } /** @@ -186,27 +259,6 @@ public class LightProbe extends Light implements Savable { this.ready = ready; } - /** - * For debuging porpose only - * Will return a Node meant to be added to a GUI presenting the 2 cube maps in a cross pattern with all the mip maps. - * - * @param manager the asset manager - * @return a debug node - */ - public Node getDebugGui(AssetManager manager) { - if (!ready) { - throw new UnsupportedOperationException("This EnvProbe is not ready yet, try to test isReady()"); - } - if (debugNode == null) { - debugNode = new Node("debug gui probe"); - Node debugPfemCm = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(getPrefilteredEnvMap(), manager); - debugNode.attachChild(debugPfemCm); - debugPfemCm.setLocalTranslation(520, 0, 0); - } - - return debugNode; - } - public Vector3f[] getShCoeffs() { return shCoeffs; } @@ -229,7 +281,7 @@ public class LightProbe extends Light implements Savable { */ public void setPosition(Vector3f position) { this.position.set(position); - getBounds().setCenter(position); + area.setCenter(position); } public int getNbMipMaps() { @@ -242,12 +294,17 @@ public class LightProbe extends Light implements Savable { @Override public boolean intersectsBox(BoundingBox box, TempVars vars) { - return getBounds().intersectsBoundingBox(box); + return area.intersectsBox(box, vars); } @Override public boolean intersectsFrustum(Camera camera, TempVars vars) { - return camera.contains(bounds) != Camera.FrustumIntersect.Outside; + return area.intersectsFrustum(camera, vars); + } + + @Override + public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) { + return area.intersectsSphere(sphere, vars); } @Override @@ -267,14 +324,8 @@ public class LightProbe extends Light implements Savable { @Override public String toString() { - return "Light Probe : " + name + " at " + position + " / " + bounds; + return "Light Probe : " + name + " at " + position + " / " + area; } - @Override - public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) { - return getBounds().intersectsSphere(sphere); - } - - } diff --git a/jme3-core/src/main/java/com/jme3/light/LightProbeBlendingProcessor.java b/jme3-core/src/main/java/com/jme3/light/LightProbeBlendingProcessor.java deleted file mode 100644 index 572fd5c18..000000000 --- a/jme3-core/src/main/java/com/jme3/light/LightProbeBlendingProcessor.java +++ /dev/null @@ -1,214 +0,0 @@ - /* - * 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.bounding.BoundingSphere; -import com.jme3.post.SceneProcessor; -import com.jme3.profile.AppProfiler; -import com.jme3.renderer.RenderManager; -import com.jme3.renderer.ViewPort; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.scene.Spatial; -import com.jme3.texture.FrameBuffer; -import com.jme3.util.TempVars; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * this processor allows to blend several light probes maps together according to a Point of Interest. - * This is all based on this article by Sebastien lagarde - * https://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/ - * @author Nehon - */ -public class LightProbeBlendingProcessor implements SceneProcessor { - - private ViewPort viewPort; - private LightFilter prevFilter; - private RenderManager renderManager; - private LightProbe probe = new LightProbe(); - private Spatial poi; - private AppProfiler prof; - - public LightProbeBlendingProcessor(Spatial poi) { - this.poi = poi; - } - - @Override - public void initialize(RenderManager rm, ViewPort vp) { - viewPort = vp; - renderManager = rm; - prevFilter = rm.getLightFilter(); - rm.setLightFilter(new PoiLightProbeLightFilter(this)); - } - - @Override - public void reshape(ViewPort vp, int w, int h) { - - } - - @Override - public boolean isInitialized() { - return viewPort != null; - } - - @Override - public void preFrame(float tpf) { - - } - - /** 1. For POI take a spatial in the constructor and make all calculation against its world pos - * - Alternatively compute an arbitrary POI by casting rays from the camera - * (one in the center and one for each corner and take the median point) - * 2. Take the 4 most weighted probes for default. Maybe allow the user to change this - * 3. For the inner influence radius take half of the radius for a start we'll see then how to change this. - * - */ - @Override - public void postQueue(RenderQueue rq) { - List blendFactors = new ArrayList(); - float sumBlendFactors = computeBlendFactors(blendFactors); - - //Sort blend factors according to their weight - Collections.sort(blendFactors); - - //normalize blend factors; - float normalizer = 1f / sumBlendFactors; - for (BlendFactor blendFactor : blendFactors) { - blendFactor.ndf *= normalizer; - // System.err.println(blendFactor); - } - - - //for now just pick the first probe. - if(!blendFactors.isEmpty()){ - probe = blendFactors.get(0).lightProbe; - }else{ - probe = null; - } - } - - private float computeBlendFactors(List blendFactors) { - float sumBlendFactors = 0; - for (Spatial scene : viewPort.getScenes()) { - for (Light light : scene.getWorldLightList()) { - if(light.getType() == Light.Type.Probe){ - LightProbe p = (LightProbe)light; - TempVars vars = TempVars.get(); - boolean intersect = p.intersectsFrustum(viewPort.getCamera(), vars); - vars.release(); - //check if the probe is inside the camera frustum - if(intersect){ - - //is the poi inside the bounds of this probe - if(poi.getWorldBound().intersects(p.getBounds())){ - - //computing the distance as we need it to check if th epoi in in the inner radius and later to compute the weight - float outerRadius = ((BoundingSphere)p.getBounds()).getRadius(); - float innerRadius = outerRadius * 0.5f; - float distance = p.getBounds().getCenter().distance(poi.getWorldTranslation()); - - // if the poi in inside the inner range of this probe, then this probe is the only one that matters. - if( distance < innerRadius ){ - blendFactors.clear(); - blendFactors.add(new BlendFactor(p, 1.0f)); - return 1.0f; - } - //else we need to compute the weight of this probe and collect it for blending - float ndf = (distance - innerRadius) / (outerRadius - innerRadius); - sumBlendFactors += ndf; - blendFactors.add(new BlendFactor(p, ndf)); - } - } - } - } - } - return sumBlendFactors; - } - - @Override - public void postFrame(FrameBuffer out) { - - } - - @Override - public void cleanup() { - viewPort = null; - renderManager.setLightFilter(prevFilter); - } - - public void populateProbe(LightList lightList){ - if(probe != null && probe.isReady()){ - lightList.add(probe); - } - } - - public Spatial getPoi() { - return poi; - } - - public void setPoi(Spatial poi) { - this.poi = poi; - } - - @Override - public void setProfiler(AppProfiler profiler) { - this.prof = profiler; - } - - private class BlendFactor implements Comparable{ - - LightProbe lightProbe; - float ndf; - - public BlendFactor(LightProbe lightProbe, float ndf) { - this.lightProbe = lightProbe; - this.ndf = ndf; - } - - @Override - public String toString() { - return "BlendFactor{" + "lightProbe=" + lightProbe + ", ndf=" + ndf + '}'; - } - - @Override - public int compareTo(BlendFactor o) { - if(o.ndf > ndf){ - return -1; - }else if(o.ndf < ndf){ - return 1; - } - return 0; - } - - } -} diff --git a/jme3-core/src/main/java/com/jme3/light/OrientedBoxProbeArea.java b/jme3-core/src/main/java/com/jme3/light/OrientedBoxProbeArea.java new file mode 100644 index 000000000..3b35c54a2 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/light/OrientedBoxProbeArea.java @@ -0,0 +1,249 @@ +package com.jme3.light; + +import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; +import com.jme3.export.*; +import com.jme3.math.*; +import com.jme3.renderer.Camera; +import com.jme3.util.TempVars; + +import java.io.IOException; + +public class OrientedBoxProbeArea implements ProbeArea { + private Transform transform = new Transform(); + + /** + * @see LightProbe#getUniformMatrix() + * for this Area type, the matrix is updated when the probe is transformed, + * and its data is used for bound checks in the light culling process. + */ + private Matrix4f uniformMatrix = new Matrix4f(); + + public OrientedBoxProbeArea() { + } + + public OrientedBoxProbeArea(Transform transform) { + transform.set(transform); + updateMatrix(); + } + + @Override + public boolean intersectsBox(BoundingBox box, TempVars vars) { + + Vector3f axis1 = getScaledAxis(0, vars.vect1); + Vector3f axis2 = getScaledAxis(1, vars.vect2); + Vector3f axis3 = getScaledAxis(2, vars.vect3); + + Vector3f tn = vars.vect4; + Plane p = vars.plane; + Vector3f c = box.getCenter(); + + p.setNormal(0, 0, -1); + p.setConstant(-(c.z + box.getZExtent())); + if (!insidePlane(p, axis1, axis2, axis3, tn)) return false; + + p.setNormal(0, 0, 1); + p.setConstant(c.z - box.getZExtent()); + if (!insidePlane(p, axis1, axis2, axis3, tn)) return false; + + + p.setNormal(0, -1, 0); + p.setConstant(-(c.y + box.getYExtent())); + if (!insidePlane(p, axis1, axis2, axis3, tn)) return false; + + p.setNormal(0, 1, 0); + p.setConstant(c.y - box.getYExtent()); + if (!insidePlane(p, axis1, axis2, axis3, tn)) return false; + + p.setNormal(-1, 0, 0); + p.setConstant(-(c.x + box.getXExtent())); + if (!insidePlane(p, axis1, axis2, axis3, tn)) return false; + + p.setNormal(1, 0, 0); + p.setConstant(c.x - box.getXExtent()); + if (!insidePlane(p, axis1, axis2, axis3, tn)) return false; + + return true; + + } + + @Override + public float getRadius() { + return Math.max(Math.max(transform.getScale().x, transform.getScale().y), transform.getScale().z); + } + + @Override + public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) { + + Vector3f closestPoint = getClosestPoint(vars, sphere.getCenter()); + // check if the point intersects with the sphere bound + if (sphere.intersects(closestPoint)) { + return true; + } + return false; + } + + @Override + public boolean intersectsFrustum(Camera camera, TempVars vars) { + + // extract the scaled axis + // this allows a small optimization. + Vector3f axis1 = getScaledAxis(0, vars.vect1); + Vector3f axis2 = getScaledAxis(1, vars.vect2); + Vector3f axis3 = getScaledAxis(2, vars.vect3); + + Vector3f tn = vars.vect4; + + for (int i = 5; i >= 0; i--) { + Plane p = camera.getWorldPlane(i); + if (!insidePlane(p, axis1, axis2, axis3, tn)) return false; + } + return true; + } + + private Vector3f getScaledAxis(int index, Vector3f store) { + Matrix4f u = uniformMatrix; + float x = 0, y = 0, z = 0, s = 1; + switch (index) { + case 0: + x = u.m00; + y = u.m10; + z = u.m20; + s = u.m30; + break; + case 1: + x = u.m01; + y = u.m11; + z = u.m21; + s = u.m31; + break; + case 2: + x = u.m02; + y = u.m12; + z = u.m22; + s = u.m32; + } + return store.set(x, y, z).multLocal(s); + } + + private boolean insidePlane(Plane p, Vector3f axis1, Vector3f axis2, Vector3f axis3, Vector3f tn) { + // transform the plane normal in the box local space. + tn.set(axis1.dot(p.getNormal()), axis2.dot(p.getNormal()), axis3.dot(p.getNormal())); + + // distance check + float radius = FastMath.abs(tn.x) + + FastMath.abs(tn.y) + + FastMath.abs(tn.z); + + float distance = p.pseudoDistance(transform.getTranslation()); + + if (distance < -radius) { + return false; + } + return true; + } + + private Vector3f getClosestPoint(TempVars vars, Vector3f point) { + // non normalized direction + Vector3f dir = vars.vect2.set(point).subtractLocal(transform.getTranslation()); + // initialize the closest point with box center + Vector3f closestPoint = vars.vect3.set(transform.getTranslation()); + + //store extent in an array + float[] r = vars.fWdU; + r[0] = transform.getScale().x; + r[1] = transform.getScale().y; + r[2] = transform.getScale().z; + + // computing closest point to sphere center + for (int i = 0; i < 3; i++) { + // extract the axis from the 3x3 matrix + Vector3f axis = getScaledAxis(i, vars.vect1); + // nomalize (here we just divide by the extent + axis.divideLocal(r[i]); + // distance to the closest point on this axis. + float d = FastMath.clamp(dir.dot(axis), -r[i], r[i]); + closestPoint.addLocal(vars.vect4.set(axis).multLocal(d)); + } + return closestPoint; + } + + private void updateMatrix() { + TempVars vars = TempVars.get(); + Matrix3f r = vars.tempMat3; + Matrix4f u = uniformMatrix; + transform.getRotation().toRotationMatrix(r); + + u.m00 = r.get(0,0); + u.m10 = r.get(1,0); + u.m20 = r.get(2,0); + u.m01 = r.get(0,1); + u.m11 = r.get(1,1); + u.m21 = r.get(2,1); + u.m02 = r.get(0,2); + u.m12 = r.get(1,2); + u.m22 = r.get(2,2); + + //scale + u.m30 = transform.getScale().x; + u.m31 = transform.getScale().y; + u.m32 = transform.getScale().z; + + //position + u.m03 = transform.getTranslation().x; + u.m13 = transform.getTranslation().y; + u.m23 = transform.getTranslation().z; + + vars.release(); + } + + public Matrix4f getUniformMatrix() { + return uniformMatrix; + } + + public Vector3f getExtent() { + return transform.getScale(); + } + + public void setExtent(Vector3f extent) { + transform.setScale(extent); + updateMatrix(); + } + + public Vector3f getCenter() { + return transform.getTranslation(); + } + + public void setCenter(Vector3f center) { + transform.setTranslation(center); + updateMatrix(); + } + + public Quaternion getRotation() { + return transform.getRotation(); + } + + public void setRotation(Quaternion rotation) { + transform.setRotation(rotation); + updateMatrix(); + } + + @Override + protected OrientedBoxProbeArea clone() throws CloneNotSupportedException { + return new OrientedBoxProbeArea(transform); + } + + @Override + public void write(JmeExporter e) throws IOException { + OutputCapsule oc = e.getCapsule(this); + oc.write(transform, "transform", new Transform()); + } + + @Override + public void read(JmeImporter i) throws IOException { + InputCapsule ic = i.getCapsule(this); + transform = (Transform) ic.readSavable("transform", new Transform()); + updateMatrix(); + } + +} diff --git a/jme3-core/src/main/java/com/jme3/light/PoiLightProbeLightFilter.java b/jme3-core/src/main/java/com/jme3/light/PoiLightProbeLightFilter.java deleted file mode 100644 index b991036ae..000000000 --- a/jme3-core/src/main/java/com/jme3/light/PoiLightProbeLightFilter.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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.bounding.BoundingBox; -import com.jme3.bounding.BoundingSphere; -import com.jme3.bounding.BoundingVolume; -import com.jme3.renderer.Camera; -import com.jme3.scene.Geometry; -import com.jme3.util.TempVars; -import java.util.HashSet; - -public final class PoiLightProbeLightFilter implements LightFilter { - - private Camera camera; - private final HashSet processedLights = new HashSet(); - private final LightProbeBlendingProcessor processor; - - public PoiLightProbeLightFilter(LightProbeBlendingProcessor processor) { - this.processor = processor; - } - - @Override - public void setCamera(Camera camera) { - this.camera = camera; - for (Light light : processedLights) { - light.frustumCheckNeeded = true; - } - } - - @Override - public void filterLights(Geometry geometry, LightList filteredLightList) { - TempVars vars = TempVars.get(); - try { - LightList worldLights = geometry.getWorldLightList(); - - for (int i = 0; i < worldLights.size(); i++) { - Light light = worldLights.get(i); - - if (light.getType() == Light.Type.Probe) { - continue; - } - - if (light.frustumCheckNeeded) { - processedLights.add(light); - light.frustumCheckNeeded = false; - light.intersectsFrustum = light.intersectsFrustum(camera, vars); - } - - if (!light.intersectsFrustum) { - continue; - } - - BoundingVolume bv = geometry.getWorldBound(); - - if (bv instanceof BoundingBox) { - if (!light.intersectsBox((BoundingBox) bv, vars)) { - continue; - } - } else if (bv instanceof BoundingSphere) { - if (!Float.isInfinite(((BoundingSphere) bv).getRadius())) { - if (!light.intersectsSphere((BoundingSphere) bv, vars)) { - continue; - } - } - } - - filteredLightList.add(light); - } - - processor.populateProbe(filteredLightList); - - } finally { - vars.release(); - } - } - -} 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 69c096790..dddf06287 100644 --- a/jme3-core/src/main/java/com/jme3/light/PointLight.java +++ b/jme3-core/src/main/java/com/jme3/light/PointLight.java @@ -212,12 +212,7 @@ public class PointLight extends Light { if (this.radius == 0) { return true; } else { - for (int i = 5; i >= 0; i--) { - if (camera.getWorldPlane(i).pseudoDistance(position) <= -radius) { - return false; - } - } - return true; + return Intersection.intersect(camera, position, radius); } } diff --git a/jme3-core/src/main/java/com/jme3/light/ProbeArea.java b/jme3-core/src/main/java/com/jme3/light/ProbeArea.java new file mode 100644 index 000000000..da0e57174 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/light/ProbeArea.java @@ -0,0 +1,33 @@ +package com.jme3.light; + +import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; +import com.jme3.export.Savable; +import com.jme3.math.Matrix4f; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.util.TempVars; + +public interface ProbeArea extends Savable, Cloneable{ + + public void setCenter(Vector3f center); + + public float getRadius(); + + public Matrix4f getUniformMatrix(); + + /** + * @see Light#intersectsBox(BoundingBox, TempVars) + */ + public boolean intersectsBox(BoundingBox box, TempVars vars); + + /** + * @see Light#intersectsSphere(BoundingSphere, TempVars) + */ + public boolean intersectsSphere(BoundingSphere sphere, TempVars vars); + + /** + * @see Light#intersectsFrustum(Camera, TempVars) + */ + public abstract boolean intersectsFrustum(Camera camera, TempVars vars); +} diff --git a/jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java b/jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java new file mode 100644 index 000000000..6fbf1a1cf --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java @@ -0,0 +1,102 @@ +package com.jme3.light; + +import com.jme3.bounding.*; +import com.jme3.export.*; +import com.jme3.math.Matrix4f; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.util.TempVars; + +import java.io.IOException; +import java.util.logging.Level; + +public class SphereProbeArea implements ProbeArea { + + private Vector3f center = new Vector3f(); + private float radius = 1; + private Matrix4f uniformMatrix = new Matrix4f(); + + public SphereProbeArea() { + } + + public SphereProbeArea(Vector3f center, float radius) { + this.center.set(center); + this.radius = radius; + updateMatrix(); + } + + public Vector3f getCenter() { + return center; + } + + public void setCenter(Vector3f center) { + this.center.set(center); + updateMatrix(); + } + + public float getRadius() { + return radius; + } + + public void setRadius(float radius) { + this.radius = radius; + updateMatrix(); + } + + @Override + public Matrix4f getUniformMatrix() { + return uniformMatrix; + } + + private void updateMatrix(){ + //position + uniformMatrix.m03 = center.x; + uniformMatrix.m13 = center.y; + uniformMatrix.m23 = center.z; + + } + + @Override + public boolean intersectsBox(BoundingBox box, TempVars vars) { + return Intersection.intersect(box, center, radius); + } + + @Override + public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) { + return Intersection.intersect(sphere, center, radius); + } + + @Override + public boolean intersectsFrustum(Camera camera, TempVars vars) { + return Intersection.intersect(camera, center, radius); + } + + @Override + public String toString() { + return "SphereProbeArea{" + + "center=" + center + + ", radius=" + radius + + '}'; + } + + @Override + protected SphereProbeArea clone() throws CloneNotSupportedException { + return new SphereProbeArea(center, radius); + } + + @Override + public void write(JmeExporter e) throws IOException { + OutputCapsule oc = e.getCapsule(this); + oc.write(center, "center", new Vector3f()); + oc.write(radius, "radius", 1); + } + + @Override + public void read(JmeImporter i) throws IOException { + InputCapsule ic = i.getCapsule(this); + center = (Vector3f) ic.readSavable("center", new Vector3f()); + radius = ic.readFloat("radius", 1); + updateMatrix(); + } + +} diff --git a/jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java b/jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java new file mode 100644 index 000000000..7e7f2f92f --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java @@ -0,0 +1,77 @@ +/* + * 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.scene.Geometry; + +import java.util.ArrayList; +import java.util.List; + +/** + * This strategy returns the 3 closest probe from the rendered object. + *

+ * Image based lighting will be blended between those probes in the shader according to their distance and range. + * + * @author Nehon + */ +public class WeightedProbeBlendingStrategy implements LightProbeBlendingStrategy { + + private final static int MAX_PROBES = 3; + List lightProbes = new ArrayList(); + + @Override + public void registerProbe(LightProbe probe) { + lightProbes.add(probe); + } + + @Override + public void populateProbes(Geometry g, LightList lightList) { + if (!lightProbes.isEmpty()) { + //The 3 first probes are the closest to the geometry since the + //light list is sorted according to the distance to the geom. + int addedProbes = 0; + for (LightProbe p : lightProbes) { + if (p.isReady() && p.isEnabled()) { + lightList.add(p); + addedProbes ++; + } + if (addedProbes == MAX_PROBES) { + break; + } + } + + //clearing the list for next pass. + lightProbes.clear(); + } + } + +} diff --git a/jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java b/jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java index b6f89f292..f80c8274a 100644 --- a/jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java +++ b/jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java @@ -42,17 +42,17 @@ import com.jme3.scene.Geometry; import com.jme3.shader.*; import com.jme3.util.TempVars; -import java.util.EnumSet; +import java.util.*; public final class SinglePassAndImageBasedLightingLogic extends DefaultTechniqueDefLogic { private static final String DEFINE_SINGLE_PASS_LIGHTING = "SINGLE_PASS_LIGHTING"; private static final String DEFINE_NB_LIGHTS = "NB_LIGHTS"; - private static final String DEFINE_INDIRECT_LIGHTING = "INDIRECT_LIGHTING"; + private static final String DEFINE_NB_PROBES = "NB_PROBES"; private static final RenderState ADDITIVE_LIGHT = new RenderState(); private final ColorRGBA ambientLightColor = new ColorRGBA(0, 0, 0, 1); - private LightProbe lightProbe = null; + private List lightProbes = new ArrayList<>(3); static { ADDITIVE_LIGHT.setBlendMode(BlendMode.AlphaAdditive); @@ -61,13 +61,13 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique private final int singlePassLightingDefineId; private final int nbLightsDefineId; - private final int indirectLightingDefineId; + private final int nbProbesDefineId; public SinglePassAndImageBasedLightingLogic(TechniqueDef techniqueDef) { super(techniqueDef); singlePassLightingDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_SINGLE_PASS_LIGHTING, VarType.Boolean); nbLightsDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_NB_LIGHTS, VarType.Int); - indirectLightingDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_INDIRECT_LIGHTING, VarType.Boolean); + nbProbesDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_NB_PROBES, VarType.Int); } @Override @@ -81,12 +81,9 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique //Though the second pass should not render IBL as it is taken care of on first pass like ambient light in phong lighting. //We cannot change the define between passes and the old technique, and for some reason the code fails on mac (renders nothing). if(lights != null) { - lightProbe = extractIndirectLights(lights, false); - if (lightProbe == null) { - defines.set(indirectLightingDefineId, false); - } else { - defines.set(indirectLightingDefineId, true); - } + lightProbes.clear(); + extractIndirectLights(lights, false); + defines.set(nbProbesDefineId, lightProbes.size()); } return super.makeCurrent(assetManager, renderManager, rendererCaps, lights, defines); @@ -113,35 +110,44 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique Uniform lightData = shader.getUniform("g_LightData"); lightData.setVector4Length(numLights * 3);//8 lights * max 3 Uniform ambientColor = shader.getUniform("g_AmbientLightColor"); + + // Matrix4f Uniform lightProbeData = shader.getUniform("g_LightProbeData"); - lightProbeData.setVector4Length(1); + Uniform lightProbeData2 = shader.getUniform("g_LightProbeData2"); + Uniform lightProbeData3 = shader.getUniform("g_LightProbeData3"); - //TODO These 2 uniforms should be packed in an array, to be able to have several probes and blend between them. Uniform shCoeffs = shader.getUniform("g_ShCoeffs"); Uniform lightProbePemMap = shader.getUniform("g_PrefEnvMap"); + Uniform shCoeffs2 = shader.getUniform("g_ShCoeffs2"); + Uniform lightProbePemMap2 = shader.getUniform("g_PrefEnvMap2"); + Uniform shCoeffs3 = shader.getUniform("g_ShCoeffs3"); + Uniform lightProbePemMap3 = shader.getUniform("g_PrefEnvMap3"); - lightProbe = null; + lightProbes.clear(); if (startIndex != 0) { // apply additive blending for 2nd and future passes rm.getRenderer().applyRenderState(ADDITIVE_LIGHT); ambientColor.setValue(VarType.Vector4, ColorRGBA.Black); }else{ - lightProbe = extractIndirectLights(lightList,true); + extractIndirectLights(lightList,true); ambientColor.setValue(VarType.Vector4, ambientLightColor); } //If there is a lightProbe in the list we force its render on the first pass - if(lightProbe != null){ - BoundingSphere s = (BoundingSphere)lightProbe.getBounds(); - lightProbeData.setVector4InArray(lightProbe.getPosition().x, lightProbe.getPosition().y, lightProbe.getPosition().z, 1f / s.getRadius() + lightProbe.getNbMipMaps(), 0); - shCoeffs.setValue(VarType.Vector3Array, lightProbe.getShCoeffs()); - //assigning new texture indexes - int pemUnit = lastTexUnit++; - rm.getRenderer().setTexture(pemUnit, lightProbe.getPrefilteredEnvMap()); - lightProbePemMap.setValue(VarType.Int, pemUnit); + if (!lightProbes.isEmpty()) { + LightProbe lightProbe = lightProbes.get(0); + lastTexUnit = setProbeData(rm, lastTexUnit, lightProbeData, shCoeffs, lightProbePemMap, lightProbe); + if (lightProbes.size() > 1) { + lightProbe = lightProbes.get(1); + lastTexUnit = setProbeData(rm, lastTexUnit, lightProbeData2, shCoeffs2, lightProbePemMap2, lightProbe); + } + if (lightProbes.size() > 2) { + lightProbe = lightProbes.get(2); + setProbeData(rm, lastTexUnit, lightProbeData3, shCoeffs3, lightProbePemMap3, lightProbe); + } } else { //Disable IBL for this pass - lightProbeData.setVector4InArray(0,0,0,-1, 0); + lightProbeData.setValue(VarType.Matrix4, LightProbe.FALLBACK_MATRIX); } int lightDataIndex = 0; @@ -222,6 +228,18 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique return curIndex; } + private int setProbeData(RenderManager rm, int lastTexUnit, Uniform lightProbeData, Uniform shCoeffs, Uniform lightProbePemMap, LightProbe lightProbe) { + + lightProbeData.setValue(VarType.Matrix4, lightProbe.getUniformMatrix()); + //setVector4InArray(lightProbe.getPosition().x, lightProbe.getPosition().y, lightProbe.getPosition().z, 1f / area.getRadius() + lightProbe.getNbMipMaps(), 0); + shCoeffs.setValue(VarType.Vector3Array, lightProbe.getShCoeffs()); + //assigning new texture indexes + int pemUnit = lastTexUnit++; + rm.getRenderer().setTexture(pemUnit, lightProbe.getPrefilteredEnvMap()); + lightProbePemMap.setValue(VarType.Int, pemUnit); + return lastTexUnit; + } + @Override public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, int lastTexUnit) { int nbRenderedLights = 0; @@ -241,9 +259,8 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique return; } - protected LightProbe extractIndirectLights(LightList lightList, boolean removeLights) { + protected void extractIndirectLights(LightList lightList, boolean removeLights) { ambientLightColor.set(0, 0, 0, 1); - LightProbe probe = null; for (int j = 0; j < lightList.size(); j++) { Light l = lightList.get(j); if (l instanceof AmbientLight) { @@ -254,7 +271,7 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique } } if (l instanceof LightProbe) { - probe = (LightProbe)l; + lightProbes.add((LightProbe) l); if(removeLights){ lightList.remove(l); j--; @@ -262,6 +279,5 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique } } ambientLightColor.a = 1.0f; - return probe; } } 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 4f5364753..7fabbc503 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 @@ -42,29 +42,39 @@ import java.nio.FloatBuffer; public class WireFrustum extends Mesh { public WireFrustum(Vector3f[] points){ + initGeom(this, points); + } + + public static Mesh makeFrustum(Vector3f[] points){ + Mesh m = new Mesh(); + initGeom(m, points); + return m; + } + + private static void initGeom(Mesh m, Vector3f[] points) { if (points != null) - setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(points)); + m.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(points)); - setBuffer(Type.Index, 2, + m.setBuffer(Type.Index, 2, new short[]{ - 0, 1, - 1, 2, - 2, 3, - 3, 0, + 0, 1, + 1, 2, + 2, 3, + 3, 0, - 4, 5, - 5, 6, - 6, 7, - 7, 4, + 4, 5, + 5, 6, + 6, 7, + 7, 4, - 0, 4, - 1, 5, - 2, 6, - 3, 7, + 0, 4, + 1, 5, + 2, 6, + 3, 7, } ); - getBuffer(Type.Index).setUsage(Usage.Static); - setMode(Mode.Lines); + m.getBuffer(Type.Index).setUsage(Usage.Static); + m.setMode(Mode.Lines); } public void update(Vector3f[] points){ diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag index 59a744ffd..91af5d6d9 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag @@ -3,7 +3,6 @@ #import "Common/ShaderLib/Parallax.glsllib" #import "Common/ShaderLib/Lighting.glsllib" - varying vec2 texCoord; #ifdef SEPARATE_TEXCOORD varying vec2 texCoord2; @@ -12,7 +11,6 @@ varying vec2 texCoord; varying vec4 Color; uniform vec4 g_LightData[NB_LIGHTS]; - uniform vec3 g_CameraPosition; uniform float m_Roughness; @@ -21,11 +19,20 @@ uniform float m_Metallic; varying vec3 wPosition; -#ifdef INDIRECT_LIGHTING -// uniform sampler2D m_IntegrateBRDF; +#if NB_PROBES >= 1 uniform samplerCube g_PrefEnvMap; uniform vec3 g_ShCoeffs[9]; - uniform vec4 g_LightProbeData; + uniform mat4 g_LightProbeData; +#endif +#if NB_PROBES >= 2 + uniform samplerCube g_PrefEnvMap2; + uniform vec3 g_ShCoeffs2[9]; + uniform mat4 g_LightProbeData2; +#endif +#if NB_PROBES == 3 + uniform samplerCube g_PrefEnvMap3; + uniform vec3 g_ShCoeffs3[9]; + uniform mat4 g_LightProbeData3; #endif #ifdef BASECOLORMAP @@ -87,6 +94,91 @@ varying vec3 wNormal; uniform float m_AlphaDiscardThreshold; #endif +float renderProbe(vec3 viewDir, vec3 normal, vec3 norm, float Roughness, vec4 diffuseColor, vec4 specularColor, float ndotv, vec3 ao, mat4 lightProbeData,vec3 shCoeffs[9],samplerCube prefEnvMap, inout vec3 color ){ + + // lightProbeData is a mat4 with this layout + // 3x3 rot mat| + // 0 1 2 | 3 + // 0 | ax bx cx | px | ) + // 1 | ay by cy | py | probe position + // 2 | az bz cz | pz | ) + // --|----------| + // 3 | sx sy sz sp | -> 1/probe radius + nbMipMaps + // --scale-- + // parallax fix for spherical / obb bounds and probe blending from + // from https://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/ + vec3 rv = reflect(-viewDir, normal); + vec4 probePos = lightProbeData[3]; + float invRadius = fract( probePos.w); + float nbMipMaps = probePos.w - invRadius; + vec3 direction = wPosition - probePos.xyz; + float ndf = 0.0; + + if(lightProbeData[0][3] != 0.0){ + // oriented box probe + mat3 wToLocalRot = mat3(lightProbeData); + wToLocalRot = inverse(wToLocalRot); + vec3 scale = vec3(lightProbeData[0][3], lightProbeData[1][3], lightProbeData[2][3]); + #if NB_PROBES >= 2 + // probe blending + // compute fragment position in probe local space + vec3 localPos = wToLocalRot * wPosition; + localPos -= probePos.xyz; + // compute normalized distance field + vec3 localDir = abs(localPos); + localDir /= scale; + ndf = max(max(localDir.x, localDir.y), localDir.z); + #endif + // parallax fix + vec3 rayLs = wToLocalRot * rv; + rayLs /= scale; + + vec3 positionLs = wPosition - probePos.xyz; + positionLs = wToLocalRot * positionLs; + positionLs /= scale; + + vec3 unit = vec3(1.0); + vec3 firstPlaneIntersect = (unit - positionLs) / rayLs; + vec3 secondPlaneIntersect = (-unit - positionLs) / rayLs; + vec3 furthestPlane = max(firstPlaneIntersect, secondPlaneIntersect); + float distance = min(min(furthestPlane.x, furthestPlane.y), furthestPlane.z); + + vec3 intersectPositionWs = wPosition + rv * distance; + rv = intersectPositionWs - probePos.xyz; + + } else { + // spherical probe + // paralax fix + rv = invRadius * direction + rv; + + #if NB_PROBES >= 2 + // probe blending + float dist = sqrt(dot(direction, direction)); + ndf = dist * invRadius; + #endif + } + + vec3 indirectDiffuse = vec3(0.0); + vec3 indirectSpecular = vec3(0.0); + indirectDiffuse = sphericalHarmonics(normal.xyz, shCoeffs) * diffuseColor.rgb; + vec3 dominantR = getSpecularDominantDir( normal, rv.xyz, Roughness * Roughness ); + indirectSpecular = ApproximateSpecularIBLPolynomial(prefEnvMap, specularColor.rgb, Roughness, ndotv, dominantR, nbMipMaps); + + #ifdef HORIZON_FADE + //horizon fade from http://marmosetco.tumblr.com/post/81245981087 + float horiz = dot(rv, norm); + float horizFadePower = 1.0 - Roughness; + horiz = clamp( 1.0 + horizFadePower * horiz, 0.0, 1.0 ); + horiz *= horiz; + indirectSpecular *= vec3(horiz); + #endif + + vec3 indirectLighting = (indirectDiffuse + indirectSpecular) * ao; + + color = indirectLighting * step( 0.0, probePos.w); + return ndf; +} + void main(){ vec2 newTexCoord; vec3 viewDir = normalize(g_CameraPosition - wPosition); @@ -250,32 +342,47 @@ void main(){ gl_FragColor.rgb += directLighting * fallOff; } - #ifdef INDIRECT_LIGHTING - vec3 rv = reflect(-viewDir.xyz, normal.xyz); - //prallax fix for spherical bounds from https://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/ - // g_LightProbeData.w is 1/probe radius + nbMipMaps, g_LightProbeData.xyz is the position of the lightProbe. - float invRadius = fract( g_LightProbeData.w); - float nbMipMaps = g_LightProbeData.w - invRadius; - rv = invRadius * (wPosition - g_LightProbeData.xyz) +rv; + #if NB_PROBES >= 1 + vec3 color1 = vec3(0.0); + vec3 color2 = vec3(0.0); + vec3 color3 = vec3(0.0); + float weight1 = 1.0; + float weight2 = 0.0; + float weight3 = 0.0; + + float ndf = renderProbe(viewDir, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData, g_ShCoeffs, g_PrefEnvMap, color1); + #if NB_PROBES >= 2 + float ndf2 = renderProbe(viewDir, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData2, g_ShCoeffs2, g_PrefEnvMap2, color2); + #endif + #if NB_PROBES == 3 + float ndf3 = renderProbe(viewDir, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData3, g_ShCoeffs3, g_PrefEnvMap3, color3); + #endif - //horizon fade from http://marmosetco.tumblr.com/post/81245981087 - float horiz = dot(rv, norm); - float horizFadePower = 1.0 - Roughness; - horiz = clamp( 1.0 + horizFadePower * horiz, 0.0, 1.0 ); - horiz *= horiz; + #if NB_PROBES >= 2 + float invNdf = max(1.0 - ndf,0.0); + float invNdf2 = max(1.0 - ndf2,0.0); + float sumNdf = ndf + ndf2; + float sumInvNdf = invNdf + invNdf2; + #if NB_PROBES == 3 + float invNdf3 = max(1.0 - ndf3,0.0); + sumNdf += ndf3; + sumInvNdf += invNdf3; + weight3 = ((1.0 - (ndf3 / sumNdf)) / (NB_PROBES - 1)) * (invNdf3 / sumInvNdf); + #endif - vec3 indirectDiffuse = vec3(0.0); - vec3 indirectSpecular = vec3(0.0); - indirectDiffuse = sphericalHarmonics(normal.xyz, g_ShCoeffs) * diffuseColor.rgb; - vec3 dominantR = getSpecularDominantDir( normal, rv.xyz, Roughness*Roughness ); - indirectSpecular = ApproximateSpecularIBLPolynomial(g_PrefEnvMap, specularColor.rgb, Roughness, ndotv, dominantR, nbMipMaps); - indirectSpecular *= vec3(horiz); + weight1 = ((1.0 - (ndf / sumNdf)) / (NB_PROBES - 1)) * (invNdf / sumInvNdf); + weight2 = ((1.0 - (ndf2 / sumNdf)) / (NB_PROBES - 1)) * (invNdf2 / sumInvNdf); - vec3 indirectLighting = (indirectDiffuse + indirectSpecular) * ao; + float weightSum = weight1 + weight2 + weight3; + + weight1 /= weightSum; + weight2 /= weightSum; + weight3 /= weightSum; + #endif + gl_FragColor.rgb += color1 * clamp(weight1,0.0,1.0) + color2 * clamp(weight2,0.0,1.0) + color3 * clamp(weight3,0.0,1.0); - gl_FragColor.rgb = gl_FragColor.rgb + indirectLighting * step( 0.0, g_LightProbeData.w); #endif - + #if defined(EMISSIVE) || defined (EMISSIVEMAP) #ifdef EMISSIVEMAP vec4 emissive = texture2D(m_EmissiveMap, newTexCoord); diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md index 437947d6c..e300bffdc 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md @@ -62,6 +62,9 @@ MaterialDef PBR Lighting { //Set to true to activate Steep Parallax mapping Boolean SteepParallax + //Horizon fade + Boolean HorizonFade + // Set to Use Lightmap Texture2D LightMap @@ -157,6 +160,7 @@ MaterialDef PBR Lighting { AO_MAP: LightMapAsAOMap NUM_MORPH_TARGETS: NumberOfMorphTargets NUM_TARGETS_BUFFERS: NumberOfTargetsBuffers + HORIZON_FADE: HorizonFade } } diff --git a/jme3-examples/src/main/java/jme3test/collision/Main.java b/jme3-examples/src/main/java/jme3test/collision/Main.java new file mode 100644 index 000000000..e69de29bb diff --git a/jme3-examples/src/main/java/jme3test/light/DlsfError.java b/jme3-examples/src/main/java/jme3test/light/DlsfError.java new file mode 100644 index 000000000..e69de29bb diff --git a/jme3-examples/src/main/java/jme3test/light/TestConeVSFrustum.java b/jme3-examples/src/main/java/jme3test/light/TestConeVSFrustum.java index 24729948d..d96edf706 100644 --- a/jme3-examples/src/main/java/jme3test/light/TestConeVSFrustum.java +++ b/jme3-examples/src/main/java/jme3test/light/TestConeVSFrustum.java @@ -102,7 +102,7 @@ public class TestConeVSFrustum extends SimpleApplication { float radius = FastMath.tan(spotLight.getSpotOuterAngle()) * spotLight.getSpotRange(); - Cylinder cylinder = new Cylinder(5, 16, 0, radius, spotLight.getSpotRange(), true, false); + Cylinder cylinder = new Cylinder(5, 16, 0.01f, radius, spotLight.getSpotRange(), true, false); geom = new Geometry("light", cylinder); geom.setMaterial(new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md")); geom.getMaterial().setColor("Diffuse", ColorRGBA.White); diff --git a/jme3-examples/src/main/java/jme3test/light/TestObbVsBounds.java b/jme3-examples/src/main/java/jme3test/light/TestObbVsBounds.java new file mode 100644 index 000000000..c32773070 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/light/TestObbVsBounds.java @@ -0,0 +1,301 @@ +/* + * 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 jme3test.light; + +import com.jme3.app.ChaseCameraAppState; +import com.jme3.app.SimpleApplication; +import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.input.KeyInput; +import com.jme3.input.MouseInput; +import com.jme3.input.controls.*; +import com.jme3.light.*; +import com.jme3.material.Material; +import com.jme3.math.*; +import com.jme3.renderer.Camera; +import com.jme3.scene.*; +import com.jme3.scene.debug.Grid; +import com.jme3.scene.debug.WireFrustum; +import com.jme3.scene.shape.*; +import com.jme3.shadow.ShadowUtil; +import com.jme3.util.TempVars; + +import java.io.File; +import java.io.IOException; + +public class TestObbVsBounds extends SimpleApplication { + + private Node ln; + private BoundingBox aabb = new BoundingBox(); + private BoundingSphere sphere = new BoundingSphere(10, new Vector3f(-30, 0, -60)); + + private final static float MOVE_SPEED = 60; + private Vector3f tmp = new Vector3f(); + private Quaternion tmpQuat = new Quaternion(); + private boolean moving, shift; + private boolean panning; + + private OrientedBoxProbeArea area = new OrientedBoxProbeArea(); + private Camera frustumCam; + + private Geometry areaGeom; + private Geometry frustumGeom; + private Geometry aabbGeom; + private Geometry sphereGeom; + + public static void main(String[] args) { + TestObbVsBounds app = new TestObbVsBounds(); + app.start(); + } + + @Override + public void simpleInitApp() { + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + frustumCam = cam.clone(); + frustumCam.setFrustumFar(25); + makeCamFrustum(); + aabb.setCenter(20, 10, -60); + aabb.setXExtent(10); + aabb.setYExtent(5); + aabb.setZExtent(3); + makeBoxWire(aabb); + makeSphereWire(sphere); + + rootNode.addLight(new DirectionalLight()); + AmbientLight al = new AmbientLight(); + al.setColor(ColorRGBA.White.mult(0.2f)); + rootNode.addLight(al); + + Grid grid = new Grid(50, 50, 5); + Geometry gridGeom = new Geometry("grid", grid); + gridGeom.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md")); + gridGeom.getMaterial().setColor("Color", ColorRGBA.Gray); + rootNode.attachChild(gridGeom); + gridGeom.setLocalTranslation(-125, -25, -125); + + area.setCenter(Vector3f.ZERO); + area.setExtent(new Vector3f(4, 8, 5)); + makeAreaGeom(); + + ln = new Node("lb"); + ln.setLocalRotation(new Quaternion(-0.18826798f, -0.38304946f, -0.12780227f, 0.895261f)); + ln.attachChild(areaGeom); + ln.setLocalScale(4,8,5); + rootNode.attachChild(ln); + + inputManager.addMapping("click", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); + inputManager.addMapping("shift", new KeyTrigger(KeyInput.KEY_LSHIFT), new KeyTrigger(KeyInput.KEY_RSHIFT)); + inputManager.addMapping("middleClick", new MouseButtonTrigger(MouseInput.BUTTON_MIDDLE)); + inputManager.addMapping("up", new MouseAxisTrigger(MouseInput.AXIS_Y, false)); + inputManager.addMapping("down", new MouseAxisTrigger(MouseInput.AXIS_Y, true)); + inputManager.addMapping("left", new MouseAxisTrigger(MouseInput.AXIS_X, true)); + inputManager.addMapping("right", new MouseAxisTrigger(MouseInput.AXIS_X, false)); + + + final Node camTarget = new Node("CamTarget"); + rootNode.attachChild(camTarget); + + ChaseCameraAppState chaser = new ChaseCameraAppState(); + chaser.setTarget(camTarget); + chaser.setMaxDistance(150); + chaser.setDefaultDistance(70); + chaser.setDefaultHorizontalRotation(FastMath.HALF_PI); + chaser.setMinVerticalRotation(-FastMath.PI); + chaser.setMaxVerticalRotation(FastMath.PI * 2); + chaser.setToggleRotationTrigger(new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + stateManager.attach(chaser); + flyCam.setEnabled(false); + + inputManager.addListener(new AnalogListener() { + public void onAnalog(String name, float value, float tpf) { + Spatial s = null; + float mult = 1; + if (moving) { + s = ln; + } + if (panning) { + s = camTarget; + mult = -1; + } + if ((moving || panning) && s != null) { + if (shift) { + if (name.equals("left")) { + tmp.set(cam.getDirection()); + s.rotate(tmpQuat.fromAngleAxis(value, tmp)); + } + if (name.equals("right")) { + tmp.set(cam.getDirection()); + s.rotate(tmpQuat.fromAngleAxis(-value, tmp)); + } + } else { + value *= MOVE_SPEED * mult; + if (name.equals("up")) { + tmp.set(cam.getUp()).multLocal(value); + s.move(tmp); + } + if (name.equals("down")) { + tmp.set(cam.getUp()).multLocal(-value); + s.move(tmp); + } + if (name.equals("left")) { + tmp.set(cam.getLeft()).multLocal(value); + s.move(tmp); + } + if (name.equals("right")) { + tmp.set(cam.getLeft()).multLocal(-value); + s.move(tmp); + } + } + } + } + }, "up", "down", "left", "right"); + + inputManager.addListener(new ActionListener() { + public void onAction(String name, boolean isPressed, float tpf) { + if (name.equals("click")) { + if (isPressed) { + moving = true; + } else { + moving = false; + } + } + if (name.equals("middleClick")) { + if (isPressed) { + panning = true; + } else { + panning = false; + } + } + if (name.equals("shift")) { + if (isPressed) { + shift = true; + } else { + shift = false; + } + } + } + }, "click", "middleClick", "shift"); + + } + + public void makeAreaGeom() { + + Vector3f[] points = new Vector3f[8]; + + for (int i = 0; i < points.length; i++) { + points[i] = new Vector3f(); + } + + points[0].set(-1, -1, 1); + points[1].set(-1, 1, 1); + points[2].set(1, 1, 1); + points[3].set(1, -1, 1); + + points[4].set(-1, -1, -1); + points[5].set(-1, 1, -1); + points[6].set(1, 1, -1); + points[7].set(1, -1, -1); + + Mesh box = WireFrustum.makeFrustum(points); + areaGeom = new Geometry("light", (Mesh)box); + areaGeom.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md")); + areaGeom.getMaterial().setColor("Color", ColorRGBA.White); + } + + public void makeCamFrustum() { + Vector3f[] points = new Vector3f[8]; + for (int i = 0; i < 8; i++) { + points[i] = new Vector3f(); + } + ShadowUtil.updateFrustumPoints2(frustumCam, points); + WireFrustum frustumShape = new WireFrustum(points); + frustumGeom = new Geometry("frustum", frustumShape); + frustumGeom.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md")); + rootNode.attachChild(frustumGeom); + } + + public void makeBoxWire(BoundingBox box) { + Vector3f[] points = new Vector3f[8]; + for (int i = 0; i < 8; i++) { + points[i] = new Vector3f(); + } + points[0].set(-1, -1, 1); + points[1].set(-1, 1, 1); + points[2].set(1, 1, 1); + points[3].set(1, -1, 1); + + points[4].set(-1, -1, -1); + points[5].set(-1, 1, -1); + points[6].set(1, 1, -1); + points[7].set(1, -1, -1); + + WireFrustum frustumShape = new WireFrustum(points); + aabbGeom = new Geometry("box", frustumShape); + aabbGeom.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md")); + aabbGeom.getMaterial().getAdditionalRenderState().setWireframe(true); + aabbGeom.setLocalTranslation(box.getCenter()); + aabbGeom.setLocalScale(box.getXExtent(), box.getYExtent(), box.getZExtent()); + rootNode.attachChild(aabbGeom); + } + + public void makeSphereWire(BoundingSphere sphere) { + + sphereGeom = new Geometry("box", new Sphere(16, 16, 10)); + sphereGeom.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md")); + sphereGeom.getMaterial().getAdditionalRenderState().setWireframe(true); + sphereGeom.setLocalTranslation(sphere.getCenter()); + rootNode.attachChild(sphereGeom); + } + + + @Override + public void simpleUpdate(float tpf) { + + area.setCenter(ln.getLocalTranslation()); + area.setRotation(ln.getLocalRotation()); + + TempVars vars = TempVars.get(); + boolean intersectBox = area.intersectsBox(aabb, vars); + boolean intersectFrustum = area.intersectsFrustum(frustumCam, vars); + boolean intersectSphere = area.intersectsSphere(sphere, vars); + vars.release(); + + boolean intersect = intersectBox || intersectFrustum || intersectSphere; + + areaGeom.getMaterial().setColor("Color", intersect ? ColorRGBA.Green : ColorRGBA.White); + sphereGeom.getMaterial().setColor("Color", intersectSphere ? ColorRGBA.Cyan : ColorRGBA.White); + frustumGeom.getMaterial().setColor("Color", intersectFrustum ? ColorRGBA.Cyan : ColorRGBA.White); + aabbGeom.getMaterial().setColor("Color", intersectBox ? ColorRGBA.Cyan : ColorRGBA.White); + + } +} diff --git a/jme3-examples/src/main/java/jme3test/light/pbr/RefEnv.java b/jme3-examples/src/main/java/jme3test/light/pbr/RefEnv.java index a88e12bb0..051202cda 100644 --- a/jme3-examples/src/main/java/jme3test/light/pbr/RefEnv.java +++ b/jme3-examples/src/main/java/jme3test/light/pbr/RefEnv.java @@ -1,7 +1,6 @@ package jme3test.light.pbr; import com.jme3.app.SimpleApplication; -import com.jme3.bounding.BoundingSphere; import com.jme3.environment.EnvironmentCamera; import com.jme3.environment.LightProbeFactory; import com.jme3.environment.generation.JobProgressAdapter; @@ -10,6 +9,7 @@ import com.jme3.input.KeyInput; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.light.LightProbe; +import com.jme3.light.SphereProbeArea; import com.jme3.material.Material; import com.jme3.math.*; import com.jme3.scene.*; @@ -127,7 +127,7 @@ public class RefEnv extends SimpleApplication { rootNode.getChild(0).setCullHint(Spatial.CullHint.Dynamic); } }); - ((BoundingSphere) probe.getBounds()).setRadius(100); + ((SphereProbeArea) probe.getArea()).setRadius(100); rootNode.addLight(probe); } diff --git a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRLighting.java b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRLighting.java index 5cb276a07..4cc36b123 100644 --- a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRLighting.java +++ b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRLighting.java @@ -42,8 +42,7 @@ import com.jme3.input.ChaseCamera; import com.jme3.input.KeyInput; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; -import com.jme3.light.DirectionalLight; -import com.jme3.light.LightProbe; +import com.jme3.light.*; import com.jme3.material.Material; import com.jme3.math.*; import com.jme3.post.FilterPostProcessor; @@ -205,7 +204,7 @@ public class TestPBRLighting extends SimpleApplication { tex = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(result.getPrefilteredEnvMap(), assetManager); } }); - ((BoundingSphere) probe.getBounds()).setRadius(100); + ((SphereProbeArea) probe.getArea()).setRadius(100); rootNode.addLight(probe); //getStateManager().getState(EnvironmentManager.class).addEnvProbe(probe); From 0cc97f5653b1ea8646f85a7504a6d55ce52b4fe2 Mon Sep 17 00:00:00 2001 From: Nehon Date: Tue, 17 Apr 2018 15:50:57 +0200 Subject: [PATCH 39/54] Deletes obsolete TestPBREnv test --- .../com/jme3/light/OrientedBoxProbeArea.java | 5 + .../main/java/com/jme3/light/ProbeArea.java | 2 + .../java/com/jme3/light/SphereProbeArea.java | 1 + .../java/jme3test/light/pbr/TestPbrEnv.java | 378 ------------------ 4 files changed, 8 insertions(+), 378 deletions(-) delete mode 100644 jme3-examples/src/main/java/jme3test/light/pbr/TestPbrEnv.java diff --git a/jme3-core/src/main/java/com/jme3/light/OrientedBoxProbeArea.java b/jme3-core/src/main/java/com/jme3/light/OrientedBoxProbeArea.java index 3b35c54a2..84bef1b3f 100644 --- a/jme3-core/src/main/java/com/jme3/light/OrientedBoxProbeArea.java +++ b/jme3-core/src/main/java/com/jme3/light/OrientedBoxProbeArea.java @@ -72,6 +72,11 @@ public class OrientedBoxProbeArea implements ProbeArea { return Math.max(Math.max(transform.getScale().x, transform.getScale().y), transform.getScale().z); } + @Override + public void setRadius(float radius) { + transform.setScale(radius, radius, radius); + } + @Override public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) { diff --git a/jme3-core/src/main/java/com/jme3/light/ProbeArea.java b/jme3-core/src/main/java/com/jme3/light/ProbeArea.java index da0e57174..28ca0c4e7 100644 --- a/jme3-core/src/main/java/com/jme3/light/ProbeArea.java +++ b/jme3-core/src/main/java/com/jme3/light/ProbeArea.java @@ -14,6 +14,8 @@ public interface ProbeArea extends Savable, Cloneable{ public float getRadius(); + public void setRadius(float radius); + public Matrix4f getUniformMatrix(); /** diff --git a/jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java b/jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java index 6fbf1a1cf..9c2d3fbe8 100644 --- a/jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java +++ b/jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java @@ -38,6 +38,7 @@ public class SphereProbeArea implements ProbeArea { return radius; } + @Override public void setRadius(float radius) { this.radius = radius; updateMatrix(); diff --git a/jme3-examples/src/main/java/jme3test/light/pbr/TestPbrEnv.java b/jme3-examples/src/main/java/jme3test/light/pbr/TestPbrEnv.java deleted file mode 100644 index eb30e2d2f..000000000 --- a/jme3-examples/src/main/java/jme3test/light/pbr/TestPbrEnv.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * 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 jme3test.light.pbr; - -import com.jme3.app.SimpleApplication; -import com.jme3.bounding.BoundingSphere; -import com.jme3.input.CameraInput; -import com.jme3.input.KeyInput; -import com.jme3.input.MouseInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.KeyTrigger; -import com.jme3.input.controls.MouseAxisTrigger; -import com.jme3.light.AmbientLight; -import com.jme3.light.DirectionalLight; -import com.jme3.material.Material; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector2f; -import com.jme3.math.Vector3f; -import com.jme3.renderer.queue.RenderQueue.ShadowMode; -import com.jme3.scene.Geometry; -import com.jme3.scene.Spatial; -import com.jme3.scene.shape.Box; -import com.jme3.scene.shape.Sphere; -import com.jme3.shadow.DirectionalLightShadowRenderer; -import com.jme3.shadow.EdgeFilteringMode; - -import com.jme3.environment.LightProbeFactory; -import com.jme3.environment.EnvironmentCamera; -import com.jme3.environment.util.LightsDebugState; -import com.jme3.light.LightProbe; -import com.jme3.material.TechniqueDef; -import com.jme3.post.FilterPostProcessor; -import com.jme3.post.filters.BloomFilter; -import com.jme3.post.filters.FXAAFilter; -import com.jme3.post.filters.ToneMapFilter; -import com.jme3.post.ssao.SSAOFilter; -import com.jme3.scene.Node; -import com.jme3.texture.plugins.ktx.KTXLoader; -import com.jme3.util.SkyFactory; -import com.jme3.util.TangentBinormalGenerator; - -public class TestPbrEnv extends SimpleApplication implements ActionListener { - - public static final int SHADOWMAP_SIZE = 1024; - private Spatial[] obj; - private Material[] mat; - private DirectionalLightShadowRenderer dlsr; - private LightsDebugState debugState; - - private EnvironmentCamera envCam; - - private Geometry ground; - private Material matGroundU; - private Material matGroundL; - - private Geometry camGeom; - - public static void main(String[] args) { - TestPbrEnv app = new TestPbrEnv(); - app.start(); - } - - - public void loadScene() { - - renderManager.setPreferredLightMode(TechniqueDef.LightMode.SinglePass); - renderManager.setSinglePassLightBatchSize(3); - obj = new Spatial[2]; - // Setup first view - - mat = new Material[2]; - mat[0] = assetManager.loadMaterial("jme3test/light/pbr/pbrMat.j3m"); - //mat[1] = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m"); - mat[1] = assetManager.loadMaterial("jme3test/light/pbr/pbrMat2.j3m"); -// mat[1].setBoolean("UseMaterialColors", true); -// mat[1].setColor("Ambient", ColorRGBA.White.mult(0.5f)); -// mat[1].setColor("Diffuse", ColorRGBA.White.clone()); - - obj[0] = new Geometry("sphere", new Sphere(30, 30, 2)); - obj[0].setShadowMode(ShadowMode.CastAndReceive); - obj[1] = new Geometry("cube", new Box(1.0f, 1.0f, 1.0f)); - obj[1].setShadowMode(ShadowMode.CastAndReceive); - TangentBinormalGenerator.generate(obj[1]); - TangentBinormalGenerator.generate(obj[0]); - -// for (int i = 0; i < 60; i++) { -// Spatial t = obj[FastMath.nextRandomInt(0, obj.length - 1)].clone(false); -// t.setName("Cube" + i); -// t.setLocalScale(FastMath.nextRandomFloat() * 10f); -// t.setMaterial(mat[FastMath.nextRandomInt(0, mat.length - 1)]); -// rootNode.attachChild(t); -// t.setLocalTranslation(FastMath.nextRandomFloat() * 200f, FastMath.nextRandomFloat() * 30f + 20, 30f * (i + 2f)); -// } - - for (int i = 0; i < 2; i++) { - Spatial t = obj[0].clone(false); - t.setName("Cube" + i); - t.setLocalScale( 10f); - t.setMaterial(mat[1].clone()); - rootNode.attachChild(t); - t.setLocalTranslation(i * 200f+ 100f, 50, 800f * (i)); - } - - Box b = new Box(1000, 2, 1000); - b.scaleTextureCoordinates(new Vector2f(20, 20)); - ground = new Geometry("soil", b); - TangentBinormalGenerator.generate(ground); - ground.setLocalTranslation(0, 10, 550); - matGroundU = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - matGroundU.setColor("Color", ColorRGBA.Green); - -// matGroundL = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); -// Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg"); -// grass.setWrap(WrapMode.Repeat); -// matGroundL.setTexture("DiffuseMap", grass); - - matGroundL = assetManager.loadMaterial("jme3test/light/pbr/pbrMat4.j3m"); - - ground.setMaterial(matGroundL); - - //ground.setShadowMode(ShadowMode.CastAndReceive); - rootNode.attachChild(ground); - - l = new DirectionalLight(); - l.setColor(ColorRGBA.White); - //l.setDirection(new Vector3f(0.5973172f, -0.16583486f, 0.7846725f).normalizeLocal()); - l.setDirection(new Vector3f(-0.2823181f, -0.41889593f, 0.863031f).normalizeLocal()); - - rootNode.addLight(l); - - AmbientLight al = new AmbientLight(); - al.setColor(ColorRGBA.White.mult(0.5f)); - // rootNode.addLight(al); - - //Spatial sky = SkyFactory.createSky(assetManager, "Scenes/Beach/FullskiesSunset0068.dds", SkyFactory.EnvMapType.CubeMap); - Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/Path.hdr", SkyFactory.EnvMapType.EquirectMap); - sky.setLocalScale(350); - - rootNode.attachChild(sky); - } - DirectionalLight l; - - @Override - public void simpleInitApp() { - assetManager.registerLoader(KTXLoader.class, "ktx"); - - - // put the camera in a bad position - cam.setLocation(new Vector3f(-52.433647f, 68.69636f, -118.60924f)); - cam.setRotation(new Quaternion(0.10294232f, 0.25269797f, -0.027049713f, 0.96167296f)); - - flyCam.setMoveSpeed(100); - - loadScene(); - - dlsr = new DirectionalLightShadowRenderer(assetManager, SHADOWMAP_SIZE, 4); - dlsr.setLight(l); - //dlsr.setLambda(0.55f); - dlsr.setShadowIntensity(0.5f); - dlsr.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON); - //dlsr.displayDebug(); - // viewPort.addProcessor(dlsr); - - FilterPostProcessor fpp = new FilterPostProcessor(assetManager); - - fpp.addFilter(new ToneMapFilter(Vector3f.UNIT_XYZ.mult(6.0f))); - SSAOFilter ssao = new SSAOFilter(); - ssao.setIntensity(5); - - fpp.addFilter(ssao); - - BloomFilter bloomFilter = new BloomFilter(); - fpp.addFilter(bloomFilter); - fpp.addFilter(new FXAAFilter()); - //viewPort.addProcessor(fpp); - - initInputs(); - -// envManager = new EnvironmentManager(); -// getStateManager().attach(envManager); -// - envCam = new EnvironmentCamera(); - getStateManager().attach(envCam); - - debugState = new LightsDebugState(); - debugState.setProbeScale(5); - getStateManager().attach(debugState); - - camGeom = new Geometry("camGeom", new Sphere(16, 16, 2)); -// Material m = new Material(assetManager, "Common/MatDefs/Misc/UnshadedNodes.j3md"); -// m.setColor("Color", ColorRGBA.Green); - Material m = assetManager.loadMaterial("jme3test/light/pbr/pbrMat3.j3m"); - camGeom.setMaterial(m); - camGeom.setLocalTranslation(0, 20, 0); - camGeom.setLocalScale(5); - rootNode.attachChild(camGeom); - - // envManager.setScene(rootNode); - -// MaterialDebugAppState debug = new MaterialDebugAppState(); -// debug.registerBinding("MatDefs/PBRLighting.frag", rootNode); -// getStateManager().attach(debug); - - flyCam.setDragToRotate(true); - setPauseOnLostFocus(false); - - // cam.lookAt(camGeom.getWorldTranslation(), Vector3f.UNIT_Y); - - } - - private void fixFLyCamInputs() { - inputManager.deleteMapping(CameraInput.FLYCAM_LEFT); - inputManager.deleteMapping(CameraInput.FLYCAM_RIGHT); - inputManager.deleteMapping(CameraInput.FLYCAM_UP); - inputManager.deleteMapping(CameraInput.FLYCAM_DOWN); - - inputManager.addMapping(CameraInput.FLYCAM_LEFT, new MouseAxisTrigger(MouseInput.AXIS_X, true)); - - inputManager.addMapping(CameraInput.FLYCAM_RIGHT, new MouseAxisTrigger(MouseInput.AXIS_X, false)); - - inputManager.addMapping(CameraInput.FLYCAM_UP, new MouseAxisTrigger(MouseInput.AXIS_Y, false)); - - inputManager.addMapping(CameraInput.FLYCAM_DOWN, new MouseAxisTrigger(MouseInput.AXIS_Y, true)); - - inputManager.addListener(flyCam, CameraInput.FLYCAM_LEFT, CameraInput.FLYCAM_RIGHT, CameraInput.FLYCAM_UP, CameraInput.FLYCAM_DOWN); - } - - private void initInputs() { - inputManager.addMapping("switchGroundMat", new KeyTrigger(KeyInput.KEY_M)); - inputManager.addMapping("snapshot", new KeyTrigger(KeyInput.KEY_SPACE)); - inputManager.addMapping("fc", new KeyTrigger(KeyInput.KEY_F)); - inputManager.addMapping("debugProbe", new KeyTrigger(KeyInput.KEY_RETURN)); - inputManager.addMapping("debugTex", new KeyTrigger(KeyInput.KEY_T)); - inputManager.addMapping("up", new KeyTrigger(KeyInput.KEY_UP)); - inputManager.addMapping("down", new KeyTrigger(KeyInput.KEY_DOWN)); - inputManager.addMapping("right", new KeyTrigger(KeyInput.KEY_RIGHT)); - inputManager.addMapping("left", new KeyTrigger(KeyInput.KEY_LEFT)); - inputManager.addMapping("delete", new KeyTrigger(KeyInput.KEY_DELETE)); - - inputManager.addListener(this, "delete","switchGroundMat", "snapshot", "debugTex", "debugProbe", "fc", "up", "down", "left", "right"); - } - - private LightProbe lastProbe; - private Node debugGui ; - - @Override - public void onAction(String name, boolean keyPressed, float tpf) { - - if (name.equals("switchGroundMat") && keyPressed) { - if (ground.getMaterial() == matGroundL) { - ground.setMaterial(matGroundU); - } else { - - ground.setMaterial(matGroundL); - } - } - - if (name.equals("snapshot") && keyPressed) { - envCam.setPosition(camGeom.getWorldTranslation()); - lastProbe = LightProbeFactory.makeProbe(envCam, rootNode, new ConsoleProgressReporter()); - ((BoundingSphere)lastProbe.getBounds()).setRadius(200); - rootNode.addLight(lastProbe); - - } - - if (name.equals("delete") && keyPressed) { - System.err.println(rootNode.getWorldLightList().size()); - rootNode.removeLight(lastProbe); - System.err.println("deleted"); - System.err.println(rootNode.getWorldLightList().size()); - } - - if (name.equals("fc") && keyPressed) { - - flyCam.setEnabled(true); - } - - if (name.equals("debugProbe") && keyPressed) { - debugState.setEnabled(!debugState.isEnabled()); - } - - if (name.equals("debugTex") && keyPressed) { - if(debugGui == null || debugGui.getParent() == null){ - debugGui = lastProbe.getDebugGui(assetManager); - debugGui.setLocalTranslation(10, 200, 0); - guiNode.attachChild(debugGui); - } else if(debugGui != null){ - debugGui.removeFromParent(); - } - } - - if (name.equals("up")) { - up = keyPressed; - } - if (name.equals("down")) { - down = keyPressed; - } - if (name.equals("right")) { - right = keyPressed; - } - if (name.equals("left")) { - left = keyPressed; - } - if (name.equals("fwd")) { - fwd = keyPressed; - } - if (name.equals("back")) { - back = keyPressed; - } - - } - boolean up = false; - boolean down = false; - boolean left = false; - boolean right = false; - boolean fwd = false; - boolean back = false; - float time = 0; - float s = 50f; - boolean initialized = false; - - @Override - public void simpleUpdate(float tpf) { - - if (!initialized) { - fixFLyCamInputs(); - initialized = true; - } - float val = tpf * s; - if (up) { - camGeom.move(0, 0, val); - } - if (down) { - camGeom.move(0, 0, -val); - - } - if (right) { - camGeom.move(-val, 0, 0); - - } - if (left) { - camGeom.move(val, 0, 0); - - } - - } - -} From ce79df93704c5bc7014488aa60a8892b573c6b1e Mon Sep 17 00:00:00 2001 From: grizeldi Date: Wed, 18 Apr 2018 16:11:42 +0200 Subject: [PATCH 40/54] Fix a typo which made pbr shader fail to compile --- .../src/main/resources/Common/MatDefs/Light/PBRLighting.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag index 91af5d6d9..283db1fd6 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag @@ -272,7 +272,7 @@ void main(){ vec4 specularColor = vec4(1.0); #endif #ifdef GLOSSINESSMAP - float glossiness = texture2D(m_GlossinesMap, newTexCoord).r * m_Glossiness; + float glossiness = texture2D(m_GlossinessMap, newTexCoord).r * m_Glossiness; #else float glossiness = m_Glossiness; #endif From 3aeb7350ae860356b73305bdf1aaf4d93335a016 Mon Sep 17 00:00:00 2001 From: Nehon Date: Thu, 19 Apr 2018 08:00:38 +0200 Subject: [PATCH 41/54] Fixes an issue with animation speed being wrongly applied --- .../main/java/com/jme3/anim/AnimComposer.java | 20 +++++++++--- .../com/jme3/anim/tween/action/Action.java | 31 +++++++++++++------ .../jme3/anim/tween/action/BaseAction.java | 15 ++++++--- .../anim/tween/action/BlendableAction.java | 11 +++++-- .../model/anim/TestAnimMigration.java | 14 ++++++--- 5 files changed, 65 insertions(+), 26 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index 4b115037b..646289367 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -144,10 +144,12 @@ public class AnimComposer extends AbstractControl { if (currentAction == null) { continue; } - layer.time += tpf; + layer.advance(tpf); + currentAction.setMask(layer.mask); - boolean running = currentAction.interpolate(layer.time * globalSpeed); + boolean running = currentAction.interpolate(layer.time); currentAction.setMask(null); + if (!running) { layer.time = 0; } @@ -214,11 +216,21 @@ public class AnimComposer extends AbstractControl { oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap()); } - public static class Layer implements JmeCloneable { + private class Layer implements JmeCloneable { private Action currentAction; private AnimationMask mask; private float weight; - private float time; + private double time; + + public void advance(float tpf) { + time += tpf * currentAction.getSpeed() * globalSpeed; + // make sure negative time is in [0, length] range + if (time < 0) { + double length = currentAction.getLength(); + time = (time % length + length) % length; + } + + } @Override public Object jmeClone() { diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java index 85bd9ae01..e4038caff 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java @@ -9,6 +9,7 @@ public abstract class Action implements Tween { private double length; private double speed = 1; private AnimationMask mask; + private boolean forward = true; protected Action(Tween... tweens) { this.actions = new Action[tweens.length]; @@ -22,16 +23,6 @@ public abstract class Action implements Tween { } } - @Override - public boolean interpolate(double t) { - t = t * speed; - // make sure negative time is in [0, length] range - t = (t % length + length) % length; - return subInterpolate(t); - } - - public abstract boolean subInterpolate(double t); - @Override public double getLength() { return length; @@ -47,6 +38,11 @@ public abstract class Action implements Tween { public void setSpeed(double speed) { this.speed = speed; + if( speed < 0){ + setForward(false); + } else { + setForward(true); + } } public AnimationMask getMask() { @@ -56,4 +52,19 @@ public abstract class Action implements Tween { public void setMask(AnimationMask mask) { this.mask = mask; } + + protected boolean isForward() { + return forward; + } + + protected void setForward(boolean forward) { + if(this.forward == forward){ + return; + } + this.forward = forward; + for (Action action : actions) { + action.setForward(forward); + } + + } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java index c3c021530..d2e891343 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java @@ -4,30 +4,35 @@ import com.jme3.anim.tween.ContainsTweens; import com.jme3.anim.tween.Tween; import com.jme3.util.SafeArrayList; +import java.util.Collections; +import java.util.List; + public class BaseAction extends Action { private Tween tween; - private SafeArrayList subActions = new SafeArrayList<>(Action.class); public BaseAction(Tween tween) { this.tween = tween; setLength(tween.getLength()); - gatherActions(tween); + List subActions = new SafeArrayList<>(Action.class); + gatherActions(tween, subActions); + actions = new Action[subActions.size()]; + subActions.toArray(actions); } - private void gatherActions(Tween tween) { + private void gatherActions(Tween tween, List subActions) { if (tween instanceof Action) { subActions.add((Action) tween); } else if (tween instanceof ContainsTweens) { Tween[] tweens = ((ContainsTweens) tween).getTweens(); for (Tween t : tweens) { - gatherActions(t); + gatherActions(t, subActions); } } } @Override - public boolean subInterpolate(double t) { + public boolean interpolate(double t) { return tween.interpolate(t); } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java index f1bcef9f2..0ba79d841 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java @@ -3,6 +3,7 @@ package com.jme3.anim.tween.action; import com.jme3.anim.tween.AbstractTween; import com.jme3.anim.tween.Tween; import com.jme3.anim.util.HasLocalTransform; +import com.jme3.math.FastMath; import com.jme3.math.Transform; import java.util.Collection; @@ -19,13 +20,12 @@ public abstract class BlendableAction extends Action { super(tweens); } - public void setCollectTransformDelegate(BlendableAction delegate) { this.collectTransformDelegate = delegate; } @Override - public boolean subInterpolate(double t) { + public boolean interpolate(double t) { // Sanity check the inputs if (t < 0) { return true; @@ -35,7 +35,12 @@ public abstract class BlendableAction extends Action { if (transition.getLength() > getLength()) { transition.setLength(getLength()); } - transition.interpolate(t); + if(isForward()) { + transition.interpolate(t); + } else { + float v = Math.max((float)(getLength() - t), 0f); + transition.interpolate(v); + } } else { transitionWeight = 1f; } diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index df19c652e..18cbb0275 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -153,7 +153,7 @@ public class TestAnimMigration extends SimpleApplication { action.getBlendSpace().setValue(blendValue); action.setSpeed(blendValue); } - System.err.println(blendValue); + //System.err.println(blendValue); } }, "blendUp", "blendDown"); } @@ -172,10 +172,15 @@ public class TestAnimMigration extends SimpleApplication { for (String name : composer.getAnimClipsNames()) { anims.add(name); } - composer.actionSequence("Sequence", + composer.actionSequence("Sequence1", composer.makeAction("Walk"), composer.makeAction("Run"), - composer.makeAction("Jumping")).setSpeed(2); + composer.makeAction("Jumping")).setSpeed(1); + + composer.actionSequence("Sequence2", + composer.makeAction("Walk"), + composer.makeAction("Run"), + composer.makeAction("Jumping")).setSpeed(-1); action = composer.actionBlended("Blend", new LinearBlendSpace(1, 4), "Walk", "Run"); @@ -186,8 +191,9 @@ public class TestAnimMigration extends SimpleApplication { composer.makeLayer("LeftArm", ArmatureMask.createMask(sc.getArmature(), "shoulder.L")); - anims.addFirst("Sequence"); anims.addFirst("Blend"); + anims.addFirst("Sequence2"); + anims.addFirst("Sequence1"); if (anims.isEmpty()) { return; From 36c1ce713f7c7d52d644265d40086a57d3824c57 Mon Sep 17 00:00:00 2001 From: Nehon Date: Thu, 19 Apr 2018 08:37:28 +0200 Subject: [PATCH 42/54] Moved the render probe function in PBR.glsllib --- .../Common/MatDefs/Light/PBRLighting.frag | 91 +------------------ .../resources/Common/ShaderLib/PBR.glsllib | 86 ++++++++++++++++++ 2 files changed, 89 insertions(+), 88 deletions(-) diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag index 283db1fd6..716d3ff7a 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.frag @@ -94,91 +94,6 @@ varying vec3 wNormal; uniform float m_AlphaDiscardThreshold; #endif -float renderProbe(vec3 viewDir, vec3 normal, vec3 norm, float Roughness, vec4 diffuseColor, vec4 specularColor, float ndotv, vec3 ao, mat4 lightProbeData,vec3 shCoeffs[9],samplerCube prefEnvMap, inout vec3 color ){ - - // lightProbeData is a mat4 with this layout - // 3x3 rot mat| - // 0 1 2 | 3 - // 0 | ax bx cx | px | ) - // 1 | ay by cy | py | probe position - // 2 | az bz cz | pz | ) - // --|----------| - // 3 | sx sy sz sp | -> 1/probe radius + nbMipMaps - // --scale-- - // parallax fix for spherical / obb bounds and probe blending from - // from https://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/ - vec3 rv = reflect(-viewDir, normal); - vec4 probePos = lightProbeData[3]; - float invRadius = fract( probePos.w); - float nbMipMaps = probePos.w - invRadius; - vec3 direction = wPosition - probePos.xyz; - float ndf = 0.0; - - if(lightProbeData[0][3] != 0.0){ - // oriented box probe - mat3 wToLocalRot = mat3(lightProbeData); - wToLocalRot = inverse(wToLocalRot); - vec3 scale = vec3(lightProbeData[0][3], lightProbeData[1][3], lightProbeData[2][3]); - #if NB_PROBES >= 2 - // probe blending - // compute fragment position in probe local space - vec3 localPos = wToLocalRot * wPosition; - localPos -= probePos.xyz; - // compute normalized distance field - vec3 localDir = abs(localPos); - localDir /= scale; - ndf = max(max(localDir.x, localDir.y), localDir.z); - #endif - // parallax fix - vec3 rayLs = wToLocalRot * rv; - rayLs /= scale; - - vec3 positionLs = wPosition - probePos.xyz; - positionLs = wToLocalRot * positionLs; - positionLs /= scale; - - vec3 unit = vec3(1.0); - vec3 firstPlaneIntersect = (unit - positionLs) / rayLs; - vec3 secondPlaneIntersect = (-unit - positionLs) / rayLs; - vec3 furthestPlane = max(firstPlaneIntersect, secondPlaneIntersect); - float distance = min(min(furthestPlane.x, furthestPlane.y), furthestPlane.z); - - vec3 intersectPositionWs = wPosition + rv * distance; - rv = intersectPositionWs - probePos.xyz; - - } else { - // spherical probe - // paralax fix - rv = invRadius * direction + rv; - - #if NB_PROBES >= 2 - // probe blending - float dist = sqrt(dot(direction, direction)); - ndf = dist * invRadius; - #endif - } - - vec3 indirectDiffuse = vec3(0.0); - vec3 indirectSpecular = vec3(0.0); - indirectDiffuse = sphericalHarmonics(normal.xyz, shCoeffs) * diffuseColor.rgb; - vec3 dominantR = getSpecularDominantDir( normal, rv.xyz, Roughness * Roughness ); - indirectSpecular = ApproximateSpecularIBLPolynomial(prefEnvMap, specularColor.rgb, Roughness, ndotv, dominantR, nbMipMaps); - - #ifdef HORIZON_FADE - //horizon fade from http://marmosetco.tumblr.com/post/81245981087 - float horiz = dot(rv, norm); - float horizFadePower = 1.0 - Roughness; - horiz = clamp( 1.0 + horizFadePower * horiz, 0.0, 1.0 ); - horiz *= horiz; - indirectSpecular *= vec3(horiz); - #endif - - vec3 indirectLighting = (indirectDiffuse + indirectSpecular) * ao; - - color = indirectLighting * step( 0.0, probePos.w); - return ndf; -} - void main(){ vec2 newTexCoord; vec3 viewDir = normalize(g_CameraPosition - wPosition); @@ -350,12 +265,12 @@ void main(){ float weight2 = 0.0; float weight3 = 0.0; - float ndf = renderProbe(viewDir, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData, g_ShCoeffs, g_PrefEnvMap, color1); + float ndf = renderProbe(viewDir, wPosition, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData, g_ShCoeffs, g_PrefEnvMap, color1); #if NB_PROBES >= 2 - float ndf2 = renderProbe(viewDir, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData2, g_ShCoeffs2, g_PrefEnvMap2, color2); + float ndf2 = renderProbe(viewDir, wPosition, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData2, g_ShCoeffs2, g_PrefEnvMap2, color2); #endif #if NB_PROBES == 3 - float ndf3 = renderProbe(viewDir, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData3, g_ShCoeffs3, g_PrefEnvMap3, color3); + float ndf3 = renderProbe(viewDir, wPosition, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData3, g_ShCoeffs3, g_PrefEnvMap3, color3); #endif #if NB_PROBES >= 2 diff --git a/jme3-core/src/main/resources/Common/ShaderLib/PBR.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/PBR.glsllib index 81d56dc95..ee4edac8e 100644 --- a/jme3-core/src/main/resources/Common/ShaderLib/PBR.glsllib +++ b/jme3-core/src/main/resources/Common/ShaderLib/PBR.glsllib @@ -121,6 +121,92 @@ vec3 ApproximateSpecularIBLPolynomial(samplerCube envMap, vec3 SpecularColor , f } +float renderProbe(vec3 viewDir, vec3 worldPos, vec3 normal, vec3 norm, float Roughness, vec4 diffuseColor, vec4 specularColor, float ndotv, vec3 ao, mat4 lightProbeData,vec3 shCoeffs[9],samplerCube prefEnvMap, inout vec3 color ){ + + // lightProbeData is a mat4 with this layout + // 3x3 rot mat| + // 0 1 2 | 3 + // 0 | ax bx cx | px | ) + // 1 | ay by cy | py | probe position + // 2 | az bz cz | pz | ) + // --|----------| + // 3 | sx sy sz sp | -> 1/probe radius + nbMipMaps + // --scale-- + // parallax fix for spherical / obb bounds and probe blending from + // from https://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/ + vec3 rv = reflect(-viewDir, normal); + vec4 probePos = lightProbeData[3]; + float invRadius = fract( probePos.w); + float nbMipMaps = probePos.w - invRadius; + vec3 direction = worldPos - probePos.xyz; + float ndf = 0.0; + + if(lightProbeData[0][3] != 0.0){ + // oriented box probe + mat3 wToLocalRot = mat3(lightProbeData); + wToLocalRot = inverse(wToLocalRot); + vec3 scale = vec3(lightProbeData[0][3], lightProbeData[1][3], lightProbeData[2][3]); + #if NB_PROBES >= 2 + // probe blending + // compute fragment position in probe local space + vec3 localPos = wToLocalRot * worldPos; + localPos -= probePos.xyz; + // compute normalized distance field + vec3 localDir = abs(localPos); + localDir /= scale; + ndf = max(max(localDir.x, localDir.y), localDir.z); + #endif + // parallax fix + vec3 rayLs = wToLocalRot * rv; + rayLs /= scale; + + vec3 positionLs = worldPos - probePos.xyz; + positionLs = wToLocalRot * positionLs; + positionLs /= scale; + + vec3 unit = vec3(1.0); + vec3 firstPlaneIntersect = (unit - positionLs) / rayLs; + vec3 secondPlaneIntersect = (-unit - positionLs) / rayLs; + vec3 furthestPlane = max(firstPlaneIntersect, secondPlaneIntersect); + float distance = min(min(furthestPlane.x, furthestPlane.y), furthestPlane.z); + + vec3 intersectPositionWs = worldPos + rv * distance; + rv = intersectPositionWs - probePos.xyz; + + } else { + // spherical probe + // paralax fix + rv = invRadius * direction + rv; + + #if NB_PROBES >= 2 + // probe blending + float dist = sqrt(dot(direction, direction)); + ndf = dist * invRadius; + #endif + } + + vec3 indirectDiffuse = vec3(0.0); + vec3 indirectSpecular = vec3(0.0); + indirectDiffuse = sphericalHarmonics(normal.xyz, shCoeffs) * diffuseColor.rgb; + vec3 dominantR = getSpecularDominantDir( normal, rv.xyz, Roughness * Roughness ); + indirectSpecular = ApproximateSpecularIBLPolynomial(prefEnvMap, specularColor.rgb, Roughness, ndotv, dominantR, nbMipMaps); + + #ifdef HORIZON_FADE + //horizon fade from http://marmosetco.tumblr.com/post/81245981087 + float horiz = dot(rv, norm); + float horizFadePower = 1.0 - Roughness; + horiz = clamp( 1.0 + horizFadePower * horiz, 0.0, 1.0 ); + horiz *= horiz; + indirectSpecular *= vec3(horiz); + #endif + + vec3 indirectLighting = (indirectDiffuse + indirectSpecular) * ao; + + color = indirectLighting * step( 0.0, probePos.w); + return ndf; +} + + From db48b98a2eeb3ca87446e56c2472d993a662d9c9 Mon Sep 17 00:00:00 2001 From: Nehon Date: Sun, 22 Apr 2018 10:29:55 +0200 Subject: [PATCH 43/54] Fixes an issue where light probes were used in phong lighting --- .../java/com/jme3/material/logic/MultiPassLightingLogic.java | 4 +--- .../com/jme3/material/logic/SinglePassLightingLogic.java | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java b/jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java index 61e9f26cb..d239681bc 100644 --- a/jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java +++ b/jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java @@ -86,7 +86,7 @@ public final class MultiPassLightingLogic extends DefaultTechniqueDefLogic { for (int i = 0; i < lights.size(); i++) { Light l = lights.get(i); - if (l instanceof AmbientLight) { + if (l.getType() == Light.Type.Ambient || l.getType() == Light.Type.Probe) { continue; } @@ -156,8 +156,6 @@ public final class MultiPassLightingLogic extends DefaultTechniqueDefLogic { lightDir.setValue(VarType.Vector4, tmpLightDirection); - break; - case Probe: break; default: throw new UnsupportedOperationException("Unknown type of light: " + l.getType()); diff --git a/jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java b/jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java index 015d6b1da..67261742e 100644 --- a/jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java +++ b/jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java @@ -106,7 +106,6 @@ public final class SinglePassLightingLogic extends DefaultTechniqueDefLogic { lightData.setVector4Length(numLights * 3);//8 lights * max 3 Uniform ambientColor = shader.getUniform("g_AmbientLightColor"); - if (startIndex != 0) { // apply additive blending for 2nd and future passes rm.getRenderer().applyRenderState(ADDITIVE_LIGHT); @@ -123,7 +122,7 @@ public final class SinglePassLightingLogic extends DefaultTechniqueDefLogic { for (curIndex = startIndex; curIndex < endIndex && curIndex < lightList.size(); curIndex++) { Light l = lightList.get(curIndex); - if (l.getType() == Light.Type.Ambient) { + if (l.getType() == Light.Type.Ambient || l.getType() == Light.Type.Probe) { endIndex++; continue; } @@ -185,8 +184,6 @@ public final class SinglePassLightingLogic extends DefaultTechniqueDefLogic { lightData.setVector4InArray(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), spotAngleCos, lightDataIndex); lightDataIndex++; break; - case Probe: - break; default: throw new UnsupportedOperationException("Unknown type of light: " + l.getType()); } From 2f683c10bc90a9acde77dd5a4d77a8628acd4261 Mon Sep 17 00:00:00 2001 From: Christopher Date: Fri, 13 Apr 2018 08:34:03 -0400 Subject: [PATCH 44/54] Updated deprecation documentation --- .../src/main/java/com/jme3/texture/Image.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java index 39c7725c0..79567b222 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Image.java +++ b/jme3-core/src/main/java/com/jme3/texture/Image.java @@ -574,14 +574,14 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ { } /** - * @see {@link #Image(com.jme3.texture.Image.Format, int, int, int, java.util.ArrayList, int[], boolean)} + * @see {@link #Image(com.jme3.texture.Image.Format, int, int, int, java.util.ArrayList, int[], com.jme3.texture.image.ColorSpace)} * @param format * @param width * @param height * @param depth * @param data * @param mipMapSizes - * @deprecated use {@link #Image(com.jme3.texture.Image.Format, int, int, int, java.util.ArrayList, int[], boolean)} + * @deprecated use {@link #Image(com.jme3.texture.Image.Format, int, int, int, java.util.ArrayList, int[], com.jme3.texture.image.ColorSpace)} */ @Deprecated public Image(Format format, int width, int height, int depth, ArrayList data, @@ -630,13 +630,13 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ { } /** - * @see {@link #Image(com.jme3.texture.Image.Format, int, int, java.nio.ByteBuffer, int[], boolean)} + * @see {@link #Image(com.jme3.texture.Image.Format, int, int, java.nio.ByteBuffer, int[], com.jme3.texture.image.ColorSpace)} * @param format * @param width * @param height * @param data * @param mipMapSizes - * @deprecated use {@link #Image(com.jme3.texture.Image.Format, int, int, java.nio.ByteBuffer, int[], boolean)} + * @deprecated use {@link #Image(com.jme3.texture.Image.Format, int, int, java.nio.ByteBuffer, int[], com.jme3.texture.image.ColorSpace)} */ @Deprecated public Image(Format format, int width, int height, ByteBuffer data, @@ -664,13 +664,13 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ { } /** - * @see {@link #Image(com.jme3.texture.Image.Format, int, int, int, java.util.ArrayList, boolean)} + * @see {@link #Image(com.jme3.texture.Image.Format, int, int, int, java.util.ArrayList, com.jme3.texture.image.ColorSpace)} * @param format * @param width * @param height * @param depth * @param data - * @deprecated use {@link #Image(com.jme3.texture.Image.Format, int, int, int, java.util.ArrayList, boolean)} + * @deprecated use {@link #Image(com.jme3.texture.Image.Format, int, int, int, java.util.ArrayList, com.jme3.texture.image.ColorSpace)} */ @Deprecated public Image(Format format, int width, int height, int depth, ArrayList data) { @@ -698,12 +698,12 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ { /** - * @see {@link #Image(com.jme3.texture.Image.Format, int, int, java.nio.ByteBuffer, boolean)} + * @see {@link #Image(com.jme3.texture.Image.Format, int, int, java.nio.ByteBuffer, com.jme3.texture.image.ColorSpace)} * @param format * @param width * @param height * @param data - * @deprecated use {@link #Image(com.jme3.texture.Image.Format, int, int, java.nio.ByteBuffer, boolean)} + * @deprecated use {@link #Image(com.jme3.texture.Image.Format, int, int, java.nio.ByteBuffer, com.jme3.texture.image.ColorSpace)} */ @Deprecated public Image(Format format, int width, int height, ByteBuffer data) { From cd7c60cc59acc7ebfdddee483b449180472299db Mon Sep 17 00:00:00 2001 From: theMinka Date: Fri, 6 Apr 2018 01:12:56 +0200 Subject: [PATCH 45/54] Fixes handling of blend equations and factors in GLRenderer (#848) * Stronger separation of BlendMode.Custom from the other blend modes * Custom blend equations and factors now only gets used on BlendMode.Custom * Updated some JavaDocs in RenderState class --- .../java/com/jme3/material/RenderState.java | 183 ++++++++---------- .../java/com/jme3/renderer/RenderContext.java | 29 +++ .../com/jme3/renderer/opengl/GLRenderer.java | 169 ++++++++++------ 3 files changed, 219 insertions(+), 162 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/material/RenderState.java b/jme3-core/src/main/java/com/jme3/material/RenderState.java index f6236efd0..d2a557c65 100644 --- a/jme3-core/src/main/java/com/jme3/material/RenderState.java +++ b/jme3-core/src/main/java/com/jme3/material/RenderState.java @@ -331,9 +331,17 @@ public class RenderState implements Cloneable, Savable { */ Exclusion, /** - * Allows for custom blending by using glBlendFuncSeparate. + * Uses the blend equations and blend factors defined by the render state. *

- * + * These attributes can be set by using the following methods: + *

    + *
  • {@link RenderState#setBlendEquation(BlendEquation)}
    + *
  • {@link RenderState#setBlendEquationAlpha(BlendEquationAlpha)}
    + *
  • {@link RenderState#setCustomBlendFactors(BlendFunc, BlendFunc, BlendFunc, BlendFunc)}
    + *
+ *

+ * Result.RGB = BlendEquation( sfactorRGB * Source.RGB , dfactorRGB * Destination.RGB )
+ * Result.A = BlendEquationAlpha( sfactorAlpha * Source.A , dfactorAlpha * Destination.A ) */ Custom } @@ -425,8 +433,6 @@ public class RenderState implements Cloneable, Savable { ADDITIONAL.applyDepthWrite = false; ADDITIONAL.applyDepthTest = false; ADDITIONAL.applyColorWrite = false; - ADDITIONAL.applyBlendEquation = false; - ADDITIONAL.applyBlendEquationAlpha = false; ADDITIONAL.applyBlendMode = false; ADDITIONAL.applyPolyOffset = false; } @@ -441,9 +447,7 @@ public class RenderState implements Cloneable, Savable { boolean colorWrite = true; boolean applyColorWrite = true; BlendEquation blendEquation = BlendEquation.Add; - boolean applyBlendEquation = true; BlendEquationAlpha blendEquationAlpha = BlendEquationAlpha.InheritColor; - boolean applyBlendEquationAlpha = true; BlendMode blendMode = BlendMode.Off; boolean applyBlendMode = true; float offsetFactor = 0; @@ -466,10 +470,10 @@ public class RenderState implements Cloneable, Savable { TestFunction frontStencilFunction = TestFunction.Always; TestFunction backStencilFunction = TestFunction.Always; int cachedHashCode = -1; - BlendFunc sfactorRGB=BlendFunc.One; - BlendFunc dfactorRGB=BlendFunc.Zero; - BlendFunc sfactorAlpha=BlendFunc.One; - BlendFunc dfactorAlpha=BlendFunc.Zero; + BlendFunc sfactorRGB = BlendFunc.One; + BlendFunc dfactorRGB = BlendFunc.One; + BlendFunc sfactorAlpha = BlendFunc.One; + BlendFunc dfactorAlpha = BlendFunc.One; public void write(JmeExporter ex) throws IOException { OutputCapsule oc = ex.getCapsule(this); @@ -507,8 +511,6 @@ public class RenderState implements Cloneable, Savable { oc.write(applyDepthWrite, "applyDepthWrite", true); oc.write(applyDepthTest, "applyDepthTest", true); oc.write(applyColorWrite, "applyColorWrite", true); - oc.write(applyBlendEquation, "applyBlendEquation", true); - oc.write(applyBlendEquationAlpha, "applyBlendEquationAlpha", true); oc.write(applyBlendMode, "applyBlendMode", true); oc.write(applyPolyOffset, "applyPolyOffset", true); oc.write(applyDepthFunc, "applyDepthFunc", true); @@ -541,9 +543,9 @@ public class RenderState implements Cloneable, Savable { depthFunc = ic.readEnum("depthFunc", TestFunction.class, TestFunction.LessOrEqual); lineWidth = ic.readFloat("lineWidth", 1); sfactorRGB = ic.readEnum("sfactorRGB", BlendFunc.class, BlendFunc.One); - dfactorAlpha = ic.readEnum("dfactorRGB", BlendFunc.class, BlendFunc.Zero); + dfactorAlpha = ic.readEnum("dfactorRGB", BlendFunc.class, BlendFunc.One); sfactorRGB = ic.readEnum("sfactorAlpha", BlendFunc.class, BlendFunc.One); - dfactorAlpha = ic.readEnum("dfactorAlpha", BlendFunc.class, BlendFunc.Zero); + dfactorAlpha = ic.readEnum("dfactorAlpha", BlendFunc.class, BlendFunc.One); applyWireFrame = ic.readBoolean("applyWireFrame", true); @@ -551,14 +553,11 @@ public class RenderState implements Cloneable, Savable { applyDepthWrite = ic.readBoolean("applyDepthWrite", true); applyDepthTest = ic.readBoolean("applyDepthTest", true); applyColorWrite = ic.readBoolean("applyColorWrite", true); - applyBlendEquation = ic.readBoolean("applyBlendEquation", true); - applyBlendEquationAlpha = ic.readBoolean("applyBlendEquationAlpha", true); applyBlendMode = ic.readBoolean("applyBlendMode", true); applyPolyOffset = ic.readBoolean("applyPolyOffset", true); applyDepthFunc = ic.readBoolean("applyDepthFunc", true); applyLineWidth = ic.readBoolean("applyLineWidth", true); - } /** @@ -615,19 +614,32 @@ public class RenderState implements Cloneable, Savable { return false; } - if (blendEquation != rs.blendEquation) { + if (blendMode != rs.blendMode) { return false; } - if (blendEquationAlpha != rs.blendEquationAlpha) { - return false; - } + if (blendMode == BlendMode.Custom) { + if (blendEquation != rs.blendEquation) { + return false; + } + if (blendEquationAlpha != rs.blendEquationAlpha) { + return false; + } - if (blendMode != rs.blendMode) { - return false; + if (sfactorRGB != rs.sfactorRGB) { + return false; + } + if (dfactorRGB != rs.dfactorRGB) { + return false; + } + if (sfactorAlpha != rs.sfactorAlpha) { + return false; + } + if (dfactorAlpha != rs.dfactorAlpha) { + return false; + } } - if (offsetEnabled != rs.offsetEnabled) { return false; } @@ -675,14 +687,6 @@ public class RenderState implements Cloneable, Savable { if(lineWidth != rs.lineWidth){ return false; } - - if (blendMode.equals(BlendMode.Custom)) { - return sfactorRGB==rs.getCustomSfactorRGB() - && dfactorRGB==rs.getCustomDfactorRGB() - && sfactorAlpha==rs.getCustomSfactorAlpha() - && dfactorAlpha==rs.getCustomDfactorAlpha(); - - } return true; } @@ -768,80 +772,68 @@ public class RenderState implements Cloneable, Savable { } /** - * Set the blending equation. + * Set the blending equation for the color component (RGB). *

- * When blending is enabled, (blendMode is not - * {@link BlendMode#Off}) the input pixel will be blended with the pixel - * already in the color buffer. The blending equation is determined by the - * {@link BlendEquation}. For example, the mode {@link BlendMode#Additive} - * and {@link BlendEquation#Add} will add the input pixel's color to the - * color already in the color buffer: + * The blending equation determines, how the RGB values of the input pixel + * will be blended with the RGB values of the pixel already in the color buffer.
+ * For example, {@link BlendEquation#Add} will add the input pixel's color + * to the color already in the color buffer: *
* Result = Source Color + Destination Color - *
- * However, the mode {@link BlendMode#Additive} - * and {@link BlendEquation#Subtract} will subtract the input pixel's color to the - * color already in the color buffer: - *
- * Result = Source Color - Destination Color + *

+ * Note: This gets only used in {@link BlendMode#Custom} mode. + * All other blend modes will ignore this setting. * - * @param blendEquation The blend equation to use. + * @param blendEquation The {@link BlendEquation} to use. */ public void setBlendEquation(BlendEquation blendEquation) { - applyBlendEquation = true; this.blendEquation = blendEquation; cachedHashCode = -1; } - + /** * Set the blending equation for the alpha component. *

- * When blending is enabled, (blendMode is not - * {@link BlendMode#Off}) the input pixel will be blended with the pixel - * already in the color buffer. The blending equation is determined by the - * {@link BlendEquation} and can be overrode for the alpha component using - * the {@link BlendEquationAlpha} . For example, the mode - * {@link BlendMode#Additive} and {@link BlendEquationAlpha#Add} will add - * the input pixel's alpha to the alpha component already in the color - * buffer: + * The alpha blending equation determines, how the alpha values of the input pixel + * will be blended with the alpha values of the pixel already in the color buffer.
+ * For example, {@link BlendEquationAlpha#Add} will add the input pixel's color + * to the color already in the color buffer: *
- * Result = Source Alpha + Destination Alpha - *
- * However, the mode {@link BlendMode#Additive} and - * {@link BlendEquationAlpha#Subtract} will subtract the input pixel's alpha - * to the alpha component already in the color buffer: - *
- * Result = Source Alpha - Destination Alpha + * Result = Source Color + Destination Color + *

+ * Note: This gets only used in {@link BlendMode#Custom} mode. + * All other blend modes will ignore this setting. * - * @param blendEquationAlpha The blend equation to use for the alpha - * component. + * @param blendEquationAlpha The {@link BlendEquationAlpha} to use. */ public void setBlendEquationAlpha(BlendEquationAlpha blendEquationAlpha) { - applyBlendEquationAlpha = true; this.blendEquationAlpha = blendEquationAlpha; cachedHashCode = -1; } - /** - * Sets the custom blend factors for BlendMode.Custom as - * defined by the appropriate BlendFunc. - * + * Sets the blend factors used for the source and destination color. + *

+ * These factors will be multiplied with the color values of the input pixel + * and the pixel already in the color buffer, before both colors gets combined by the {@link BlendEquation}. + *

+ * Note: This gets only used in {@link BlendMode#Custom} mode. + * All other blend modes will ignore this setting. + * * @param sfactorRGB The source blend factor for RGB components. * @param dfactorRGB The destination blend factor for RGB components. * @param sfactorAlpha The source blend factor for the alpha component. * @param dfactorAlpha The destination blend factor for the alpha component. */ - public void setCustomBlendFactors(BlendFunc sfactorRGB, BlendFunc dfactorRGB, BlendFunc sfactorAlpha, BlendFunc dfactorAlpha) - { + public void setCustomBlendFactors(BlendFunc sfactorRGB, BlendFunc dfactorRGB, BlendFunc sfactorAlpha, BlendFunc dfactorAlpha) { this.sfactorRGB = sfactorRGB; this.dfactorRGB = dfactorRGB; this.sfactorAlpha = sfactorAlpha; this.dfactorAlpha = dfactorAlpha; cachedHashCode = -1; } - - + + /** * Enable depth testing. * @@ -1374,14 +1366,6 @@ public class RenderState implements Cloneable, Savable { return applyBlendMode; } - public boolean isApplyBlendEquation() { - return applyBlendEquation; - } - - public boolean isApplyBlendEquationAlpha() { - return applyBlendEquationAlpha; - } - public boolean isApplyColorWrite() { return applyColorWrite; } @@ -1511,27 +1495,26 @@ public class RenderState implements Cloneable, Savable { } else { state.colorWrite = colorWrite; } - if (additionalState.applyBlendEquation) { - state.blendEquation = additionalState.blendEquation; - } else { - state.blendEquation = blendEquation; - } - if (additionalState.applyBlendEquationAlpha) { - state.blendEquationAlpha = additionalState.blendEquationAlpha; - } else { - state.blendEquationAlpha = blendEquationAlpha; - } if (additionalState.applyBlendMode) { state.blendMode = additionalState.blendMode; - if (additionalState.getBlendMode().equals(BlendMode.Custom)) { - state.setCustomBlendFactors( - additionalState.getCustomSfactorRGB(), - additionalState.getCustomDfactorRGB(), - additionalState.getCustomSfactorAlpha(), - additionalState.getCustomDfactorAlpha()); + if (additionalState.blendMode == BlendMode.Custom) { + state.blendEquation = additionalState.blendEquation; + state.blendEquationAlpha = additionalState.blendEquationAlpha; + state.sfactorRGB = additionalState.sfactorRGB; + state.dfactorRGB = additionalState.dfactorRGB; + state.sfactorAlpha = additionalState.sfactorAlpha; + state.dfactorAlpha = additionalState.dfactorAlpha; } } else { state.blendMode = blendMode; + if (blendMode == BlendMode.Custom) { + state.blendEquation = blendEquation; + state.blendEquationAlpha = blendEquationAlpha; + state.sfactorRGB = sfactorRGB; + state.dfactorRGB = dfactorRGB; + state.sfactorAlpha = sfactorAlpha; + state.dfactorAlpha = dfactorAlpha; + } } if (additionalState.applyPolyOffset) { @@ -1608,8 +1591,6 @@ public class RenderState implements Cloneable, Savable { applyDepthWrite = true; applyDepthTest = true; applyColorWrite = true; - applyBlendEquation = true; - applyBlendEquationAlpha = true; applyBlendMode = true; applyPolyOffset = true; applyDepthFunc = true; @@ -1636,8 +1617,6 @@ public class RenderState implements Cloneable, Savable { + "\ncolorWrite=" + colorWrite + "\napplyColorWrite=" + applyColorWrite + "\nblendEquation=" + blendEquation - + "\napplyBlendEquation=" + applyBlendEquation - + "\napplyBlendEquationAlpha=" + applyBlendEquationAlpha + "\nblendMode=" + blendMode + "\napplyBlendMode=" + applyBlendMode + "\noffsetEnabled=" + offsetEnabled 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 49f25240a..4599fdbb3 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java @@ -32,6 +32,7 @@ package com.jme3.renderer; import com.jme3.material.RenderState; +import com.jme3.material.RenderState.BlendFunc; import com.jme3.math.ColorRGBA; import com.jme3.scene.Mesh; import com.jme3.scene.VertexBuffer; @@ -110,6 +111,30 @@ public class RenderContext { */ public RenderState.BlendEquationAlpha blendEquationAlpha = RenderState.BlendEquationAlpha.InheritColor; + /** + * @see RenderState#setCustomBlendFactors(com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc, + * com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc) + */ + public RenderState.BlendFunc sfactorRGB = RenderState.BlendFunc.One; + + /** + * @see RenderState#setCustomBlendFactors(com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc, + * com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc) + */ + public RenderState.BlendFunc dfactorRGB = RenderState.BlendFunc.One; + + /** + * @see RenderState#setCustomBlendFactors(com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc, + * com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc) + */ + public RenderState.BlendFunc sfactorAlpha = RenderState.BlendFunc.One; + + /** + * @see RenderState#setCustomBlendFactors(com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc, + * com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc) + */ + public RenderState.BlendFunc dfactorAlpha = RenderState.BlendFunc.One; + /** * @see RenderState#setWireframe(boolean) */ @@ -266,6 +291,10 @@ public class RenderContext { blendMode = RenderState.BlendMode.Off; blendEquation = RenderState.BlendEquation.Add; blendEquationAlpha = RenderState.BlendEquationAlpha.InheritColor; + sfactorRGB = BlendFunc.One; + dfactorRGB = BlendFunc.One; + sfactorAlpha = BlendFunc.One; + dfactorAlpha = BlendFunc.One; wireframe = false; boundShaderProgram = 0; boundShader = null; 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 9851f3418..746db4084 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 @@ -33,6 +33,7 @@ package com.jme3.renderer.opengl; import com.jme3.material.RenderState; import com.jme3.material.RenderState.BlendFunc; +import com.jme3.material.RenderState.BlendMode; import com.jme3.material.RenderState.StencilOperation; import com.jme3.material.RenderState.TestFunction; import com.jme3.math.*; @@ -743,68 +744,57 @@ public final class GLRenderer implements Renderer { context.cullMode = state.getFaceCullMode(); } - if (state.getBlendMode() != context.blendMode) { - if (state.getBlendMode() == RenderState.BlendMode.Off) { - gl.glDisable(GL.GL_BLEND); - } else { - if (context.blendMode == RenderState.BlendMode.Off) { - 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 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 Color: - 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; - case Custom: - gl.glBlendFuncSeparate( - convertBlendFunc(state.getCustomSfactorRGB()), - convertBlendFunc(state.getCustomDfactorRGB()), - convertBlendFunc(state.getCustomSfactorAlpha()), - convertBlendFunc(state.getCustomDfactorAlpha())); - break; - default: - throw new UnsupportedOperationException("Unrecognized blend mode: " - + state.getBlendMode()); - } - - if (state.getBlendEquation() != context.blendEquation || state.getBlendEquationAlpha() != context.blendEquationAlpha) { - int colorMode = convertBlendEquation(state.getBlendEquation()); - int alphaMode; - if (state.getBlendEquationAlpha() == RenderState.BlendEquationAlpha.InheritColor) { - alphaMode = colorMode; - } else { - alphaMode = convertBlendEquationAlpha(state.getBlendEquationAlpha()); - } - gl.glBlendEquationSeparate(colorMode, alphaMode); - context.blendEquation = state.getBlendEquation(); - context.blendEquationAlpha = state.getBlendEquationAlpha(); - } + // Always update the blend equations and factors when using custom blend mode. + if (state.getBlendMode() == BlendMode.Custom) { + changeBlendMode(BlendMode.Custom); + + blendFuncSeparate( + state.getCustomSfactorRGB(), + state.getCustomDfactorRGB(), + state.getCustomSfactorAlpha(), + state.getCustomDfactorAlpha()); + blendEquationSeparate(state.getBlendEquation(), state.getBlendEquationAlpha()); + + // Update the blend equations and factors only on a mode change for all the other (common) blend modes. + } else if (state.getBlendMode() != context.blendMode) { + changeBlendMode(state.getBlendMode()); + + switch (state.getBlendMode()) { + case Off: + break; + case Additive: + blendFunc(RenderState.BlendFunc.One, RenderState.BlendFunc.One); + break; + case AlphaAdditive: + blendFunc(RenderState.BlendFunc.Src_Alpha, RenderState.BlendFunc.One); + break; + case Alpha: + blendFunc(RenderState.BlendFunc.Src_Alpha, RenderState.BlendFunc.One_Minus_Src_Alpha); + break; + case PremultAlpha: + blendFunc(RenderState.BlendFunc.One, RenderState.BlendFunc.One_Minus_Src_Alpha); + break; + case Modulate: + blendFunc(RenderState.BlendFunc.Dst_Color, RenderState.BlendFunc.Zero); + break; + case ModulateX2: + blendFunc(RenderState.BlendFunc.Dst_Color, RenderState.BlendFunc.Src_Color); + break; + case Color: + case Screen: + blendFunc(RenderState.BlendFunc.One, RenderState.BlendFunc.One_Minus_Src_Color); + break; + case Exclusion: + blendFunc(RenderState.BlendFunc.One_Minus_Dst_Color, RenderState.BlendFunc.One_Minus_Src_Color); + break; + default: + throw new UnsupportedOperationException("Unrecognized blend mode: " + + state.getBlendMode()); } - context.blendMode = state.getBlendMode(); + // All of the common modes requires the ADD equation. + // (This might change in the future?) + blendEquationSeparate(RenderState.BlendEquation.Add, RenderState.BlendEquationAlpha.InheritColor); } if (context.stencilTest != state.isStencilTest() @@ -852,6 +842,65 @@ public final class GLRenderer implements Renderer { } } + private void changeBlendMode(RenderState.BlendMode blendMode) { + if (blendMode != context.blendMode) { + if (blendMode == RenderState.BlendMode.Off) { + gl.glDisable(GL.GL_BLEND); + } else if (context.blendMode == RenderState.BlendMode.Off) { + gl.glEnable(GL.GL_BLEND); + } + + context.blendMode = blendMode; + } + } + + private void blendEquationSeparate(RenderState.BlendEquation blendEquation, RenderState.BlendEquationAlpha blendEquationAlpha) { + if (blendEquation != context.blendEquation || blendEquationAlpha != context.blendEquationAlpha) { + int glBlendEquation = convertBlendEquation(blendEquation); + int glBlendEquationAlpha = blendEquationAlpha == RenderState.BlendEquationAlpha.InheritColor + ? glBlendEquation + : convertBlendEquationAlpha(blendEquationAlpha); + gl.glBlendEquationSeparate(glBlendEquation, glBlendEquationAlpha); + context.blendEquation = blendEquation; + context.blendEquationAlpha = blendEquationAlpha; + } + } + + private void blendFunc(RenderState.BlendFunc sfactor, RenderState.BlendFunc dfactor) { + if (sfactor != context.sfactorRGB + || dfactor != context.dfactorRGB + || sfactor != context.sfactorAlpha + || dfactor != context.dfactorAlpha) { + + gl.glBlendFunc( + convertBlendFunc(sfactor), + convertBlendFunc(dfactor)); + context.sfactorRGB = sfactor; + context.dfactorRGB = dfactor; + context.sfactorAlpha = sfactor; + context.dfactorAlpha = dfactor; + } + } + + private void blendFuncSeparate(RenderState.BlendFunc sfactorRGB, RenderState.BlendFunc dfactorRGB, + RenderState.BlendFunc sfactorAlpha, RenderState.BlendFunc dfactorAlpha) { + if (sfactorRGB != context.sfactorRGB + || dfactorRGB != context.dfactorRGB + || sfactorAlpha != context.sfactorAlpha + || dfactorAlpha != context.dfactorAlpha) { + + gl.glBlendFuncSeparate( + convertBlendFunc(sfactorRGB), + convertBlendFunc(dfactorRGB), + convertBlendFunc(sfactorAlpha), + convertBlendFunc(dfactorAlpha)); + context.sfactorRGB = sfactorRGB; + context.dfactorRGB = dfactorRGB; + context.sfactorAlpha = sfactorAlpha; + context.dfactorAlpha = dfactorAlpha; + } + } + private int convertBlendEquation(RenderState.BlendEquation blendEquation) { switch (blendEquation) { case Add: From dd561a0c5bd72751f5f84840290eeb0d47814db0 Mon Sep 17 00:00:00 2001 From: theMinka Date: Sat, 7 Apr 2018 14:49:25 +0200 Subject: [PATCH 46/54] Reworked the TestBlendEquations example (#848) --- .../jme3test/renderer/TestBlendEquations.java | 139 +++++++++++------- 1 file changed, 85 insertions(+), 54 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/renderer/TestBlendEquations.java b/jme3-examples/src/main/java/jme3test/renderer/TestBlendEquations.java index 9335912b6..435d57db3 100644 --- a/jme3-examples/src/main/java/jme3test/renderer/TestBlendEquations.java +++ b/jme3-examples/src/main/java/jme3test/renderer/TestBlendEquations.java @@ -36,75 +36,106 @@ import com.jme3.light.DirectionalLight; import com.jme3.material.Material; import com.jme3.material.RenderState; import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; import com.jme3.math.Vector3f; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.Geometry; +import com.jme3.scene.Spatial; import com.jme3.scene.shape.Quad; +/** + * This test demonstrates the usage of customized blend equations and factors on a material.
+ * Customized blend equations and factors always requires {@link RenderState.BlendMode#Custom}. + * + * @author the_Minka + */ public class TestBlendEquations extends SimpleApplication { + private Geometry leftQuad; + private Geometry rightQuad; + + private float timer; + public static void main(String[] args) { TestBlendEquations app = new TestBlendEquations(); app.start(); } public void simpleInitApp() { - Geometry teaGeom = (Geometry) assetManager.loadModel("Models/Teapot/Teapot.obj"); - teaGeom.scale(6); - teaGeom.getMaterial().getAdditionalRenderState().setBlendEquation(RenderState.BlendEquation.Add); - teaGeom.move(0, -2f, 0); - - DirectionalLight dl = new DirectionalLight(); - dl.setColor(ColorRGBA.Red); - dl.setDirection(Vector3f.UNIT_XYZ.negate()); - - rootNode.addLight(dl); - rootNode.attachChild(teaGeom); - - Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", new ColorRGBA(0.5f, 0f, 1f, 0.3f)); - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Color); - mat.getAdditionalRenderState().setBlendEquation(RenderState.BlendEquation.Subtract); - - Geometry geo = new Geometry("BottomLeft", new Quad(guiViewPort.getCamera().getWidth() / 2, guiViewPort.getCamera().getHeight() / 2)); - geo.setMaterial(mat); - geo.setQueueBucket(RenderQueue.Bucket.Gui); - geo.setLocalTranslation(0, 0, 1); - - guiNode.attachChild(geo); - - Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - m.getAdditionalRenderState().setBlendEquation(RenderState.BlendEquation.ReverseSubtract); - m.setColor("Color", new ColorRGBA(0.0f, 1f, 1.f, 1f)); - m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.AlphaAdditive); - - geo = new Geometry("BottomRight", new Quad(guiViewPort.getCamera().getWidth() / 2, guiViewPort.getCamera().getHeight() / 2)); - geo.setMaterial(m); - geo.setQueueBucket(RenderQueue.Bucket.Gui); - geo.setLocalTranslation(guiViewPort.getCamera().getWidth() / 2, 0, 1); - - guiNode.attachChild(geo); - - m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - m.getAdditionalRenderState().setBlendEquation(RenderState.BlendEquation.Min); - m.setColor("Color", new ColorRGBA(0.3f, 0f, 0.1f, 0.3f)); - m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Additive); - - geo = new Geometry("TopRight", new Quad(guiViewPort.getCamera().getWidth() / 2, guiViewPort.getCamera().getHeight() / 2)); - geo.setMaterial(m); - geo.setQueueBucket(RenderQueue.Bucket.Gui); - geo.setLocalTranslation(guiViewPort.getCamera().getWidth() / 2, guiViewPort.getCamera().getHeight() / 2, 1); - - guiNode.attachChild(geo); - - geo = new Geometry("OverTeaPot", new Quad(guiViewPort.getCamera().getWidth() / 2, guiViewPort.getCamera().getHeight() / 2)); - geo.setMaterial(mat); - geo.setQueueBucket(RenderQueue.Bucket.Transparent); - geo.setLocalTranslation(0, -100, 5); - - rootNode.attachChild(geo); + cam.setLocation(new Vector3f(0f, 0.5f, 3f)); + viewPort.setBackgroundColor(ColorRGBA.LightGray); + + // Add a light source to the scene. + DirectionalLight directionalLight = new DirectionalLight(); + directionalLight.setColor(ColorRGBA.Magenta); + directionalLight.setDirection(Vector3f.UNIT_XYZ.negate()); + rootNode.addLight(directionalLight); + + + // Create and add a teapot to the scene graph. + Spatial teapotModel = assetManager.loadModel("Models/Teapot/Teapot.obj"); + rootNode.attachChild(teapotModel); + // Create the two moving quads with custom blend modes. + createLeftQuad(); + createRightQuad(); } + /** + * Adds a "transparent" quad to the scene, that shows an inverse blue value sight of the scene behind. + */ + private void createLeftQuad() { + Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + + // This color creates a blue value image. The effect will have a strength of 80% (set by the alpha value). + material.setColor("Color", new ColorRGBA(0f, 0f, 1f, 0.8f)); + + // Result.RGB = Source.A * Source.RGB - Source.A * Destination.RGB + // Result.A = Destination.A + material.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Custom); + material.getAdditionalRenderState().setBlendEquation(RenderState.BlendEquation.Subtract); + material.getAdditionalRenderState().setBlendEquationAlpha(RenderState.BlendEquationAlpha.Add); + material.getAdditionalRenderState().setCustomBlendFactors( + RenderState.BlendFunc.Src_Alpha, RenderState.BlendFunc.Src_Alpha, + RenderState.BlendFunc.Zero, RenderState.BlendFunc.One); + + leftQuad = new Geometry("LeftQuad", new Quad(1f, 1f)); + leftQuad.setMaterial(material); + leftQuad.setQueueBucket(RenderQueue.Bucket.Transparent); + rootNode.attachChild(leftQuad); + } + + /** + * Adds a "transparent" quad to the scene, that limits the color values of the scene behind the object.
+ * This effect can be good seen on bright areas of the scene (e.g. areas with specular lighting effects). + */ + private void createRightQuad() { + Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setColor("Color", new ColorRGBA(0.4f, 0.4f, 0.4f, 1f)); + + // Min( Source , Destination) + material.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Custom); + material.getAdditionalRenderState().setBlendEquation(RenderState.BlendEquation.Min); + material.getAdditionalRenderState().setBlendEquationAlpha(RenderState.BlendEquationAlpha.Min); + + // In OpenGL no blend factors are used, when using the blend equations Min or Max! + //material.getAdditionalRenderState().setCustomBlendFactors( + // RenderState.BlendFunc.One, RenderState.BlendFunc.One, + // RenderState.BlendFunc.One, RenderState.BlendFunc.One); + + rightQuad = new Geometry("RightQuad", new Quad(1f, 1f)); + rightQuad.setMaterial(material); + rightQuad.setQueueBucket(RenderQueue.Bucket.Transparent); + rootNode.attachChild(rightQuad); + } + + @Override + public void simpleUpdate(float tpf) { + timer += tpf; + + float xOffset = FastMath.sin(timer * 0.5f) * 2f; + leftQuad.setLocalTranslation(xOffset - 2f, 0f, 0.5f); + rightQuad.setLocalTranslation(xOffset + 1f, 0f, 0.5f); + } } From 0d5f18c3a8da22151e830b4b0b8e5a1f27bfcb18 Mon Sep 17 00:00:00 2001 From: b00nation <25694121+b00nation@users.noreply.github.com> Date: Thu, 8 Mar 2018 14:36:23 +0100 Subject: [PATCH 47/54] Update MaterialDebugAppState.java I was debugging my application to dig down the issue why the shaders are recognized of change but not actually reloaded. I came to this solution. --- .../src/main/java/com/jme3/util/MaterialDebugAppState.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java b/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java index 01464f67b..b0f3565e4 100644 --- a/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java @@ -200,7 +200,8 @@ public class MaterialDebugAppState extends AbstractAppState { assetManager.clearCache(); //creating a dummy mat with the mat def of the mat to reload - Material dummy = new Material(mat.getMaterialDef()); + // Force the reloading of the asset, otherwise the new shader code will not be applied. + Material dummy = new Material(assetManager, mat.getMaterialDef().getAssetName()); for (MatParam matParam : mat.getParams()) { dummy.setParam(matParam.getName(), matParam.getVarType(), matParam.getValue()); From c0cdf75603460b8f069eeb35cb0400a68148e3ce Mon Sep 17 00:00:00 2001 From: oualid Date: Thu, 26 Apr 2018 12:15:15 +0100 Subject: [PATCH 48/54] replace addState() with attach() in the javadoc --- .../src/common/java/com/jme3/bullet/BulletAppState.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/BulletAppState.java b/jme3-bullet/src/common/java/com/jme3/bullet/BulletAppState.java index ae278c871..addea7a5e 100644 --- a/jme3-bullet/src/common/java/com/jme3/bullet/BulletAppState.java +++ b/jme3-bullet/src/common/java/com/jme3/bullet/BulletAppState.java @@ -67,7 +67,7 @@ public class BulletAppState implements AppState, PhysicsTickListener { /** * Creates a new BulletAppState running a PhysicsSpace for physics - * simulation, use getStateManager().addState(bulletAppState) to enable + * simulation, use getStateManager().attach(bulletAppState) to enable * physics for an Application. */ public BulletAppState() { @@ -75,7 +75,7 @@ public class BulletAppState implements AppState, PhysicsTickListener { /** * Creates a new BulletAppState running a PhysicsSpace for physics - * simulation, use getStateManager().addState(bulletAppState) to enable + * simulation, use getStateManager().attach(bulletAppState) to enable * physics for an Application. * * @param broadphaseType The type of broadphase collision detection, @@ -87,7 +87,7 @@ public class BulletAppState implements AppState, PhysicsTickListener { /** * Creates a new BulletAppState running a PhysicsSpace for physics - * simulation, use getStateManager().addState(bulletAppState) to enable + * simulation, use getStateManager().attach(bulletAppState) to enable * physics for an Application. An AxisSweep broadphase is used. * * @param worldMin The minimum world extent From 302e746a944f8f536fbb33fb059805e44d72c987 Mon Sep 17 00:00:00 2001 From: Nehon Date: Tue, 1 May 2018 09:14:34 +0200 Subject: [PATCH 49/54] Uses a HashSet for variable names in ShaderNodeLoaderDelegate instead of a String --- .../plugins/ShaderNodeLoaderDelegate.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/jme3-core/src/plugins/java/com/jme3/material/plugins/ShaderNodeLoaderDelegate.java b/jme3-core/src/plugins/java/com/jme3/material/plugins/ShaderNodeLoaderDelegate.java index 9eecd12b7..c8c674046 100644 --- a/jme3-core/src/plugins/java/com/jme3/material/plugins/ShaderNodeLoaderDelegate.java +++ b/jme3-core/src/plugins/java/com/jme3/material/plugins/ShaderNodeLoaderDelegate.java @@ -43,10 +43,7 @@ import com.jme3.shader.*; import com.jme3.util.blockparser.Statement; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * This class is here to be able to load shaderNodeDefinition from both the @@ -75,7 +72,7 @@ public class ShaderNodeLoaderDelegate { protected MaterialDef materialDef; protected String shaderLanguage; protected String shaderName; - protected String varNames = ""; + protected Set varNames = new HashSet<>(); protected AssetManager assetManager; protected ConditionParser conditionParser = new ConditionParser(); protected List nulledConditions = new ArrayList(); @@ -177,7 +174,7 @@ public class ShaderNodeLoaderDelegate { shaderNodeDefinition.setDocumentation(doc); } } else if (line.startsWith("Input")) { - varNames = ""; + varNames.clear(); for (Statement statement1 : statement.getContents()) { try { shaderNodeDefinition.getInputs().add(readVariable(statement1)); @@ -186,7 +183,7 @@ public class ShaderNodeLoaderDelegate { } } } else if (line.startsWith("Output")) { - varNames = ""; + varNames.clear(); for (Statement statement1 : statement.getContents()) { try { if (statement1.getLine().trim().equals("None")) { @@ -235,11 +232,11 @@ public class ShaderNodeLoaderDelegate { multiplicity = arr[1].replaceAll("\\]", "").trim(); } - if (varNames.contains(varName + ";")) { + if (varNames.contains(varName)) { throw new MatParseException("Duplicate variable name " + varName, statement); } - varNames += varName + ";"; + varNames.add(varName); final ShaderNodeVariable variable = new ShaderNodeVariable(varType, "", varName, multiplicity); variable.setDefaultValue(defaultValue); @@ -1139,7 +1136,7 @@ public class ShaderNodeLoaderDelegate { materialDef = null; shaderLanguage = ""; shaderName = ""; - varNames = ""; + varNames.clear(); assetManager = null; nulledConditions.clear(); } From 266d8b0828fe33d047a2b013bbdd7477301d1111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Bouquet?= Date: Fri, 18 May 2018 08:28:38 +0200 Subject: [PATCH 50/54] Implements SSBO (Shader Storage Buffer Object) and UBO (Uniform Buffer Object) support --- .../main/java/com/jme3/material/Material.java | 75 +- .../src/main/java/com/jme3/renderer/Caps.java | 10 +- .../main/java/com/jme3/renderer/Limits.java | 16 + .../main/java/com/jme3/renderer/Renderer.java | 15 + .../java/com/jme3/renderer/opengl/GL.java | 479 +++++----- .../java/com/jme3/renderer/opengl/GL3.java | 83 ++ .../java/com/jme3/renderer/opengl/GL4.java | 50 ++ .../jme3/renderer/opengl/GLDebugDesktop.java | 32 + .../com/jme3/renderer/opengl/GLRenderer.java | 197 ++++- .../java/com/jme3/shader/BufferObject.java | 828 ++++++++++++++++++ .../com/jme3/shader/BufferObjectField.java | 77 ++ .../src/main/java/com/jme3/shader/Shader.java | 64 +- .../com/jme3/shader/ShaderBufferBlock.java | 93 ++ .../main/java/com/jme3/shader/VarType.java | 3 +- .../java/com/jme3/system/NullRenderer.java | 15 +- .../main/java/com/jme3/util/NativeObject.java | 3 +- .../java/com/jme3/renderer/jogl/JoglGL.java | 33 +- .../java/com/jme3/renderer/lwjgl/LwjglGL.java | 35 +- .../java/com/jme3/renderer/lwjgl/LwjglGL.java | 25 + 19 files changed, 1836 insertions(+), 297 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/shader/BufferObject.java create mode 100644 jme3-core/src/main/java/com/jme3/shader/BufferObjectField.java create mode 100644 jme3-core/src/main/java/com/jme3/shader/ShaderBufferBlock.java diff --git a/jme3-core/src/main/java/com/jme3/material/Material.java b/jme3-core/src/main/java/com/jme3/material/Material.java index e2b030e93..ddd75ace7 100644 --- a/jme3-core/src/main/java/com/jme3/material/Material.java +++ b/jme3-core/src/main/java/com/jme3/material/Material.java @@ -46,10 +46,7 @@ import com.jme3.renderer.RenderManager; import com.jme3.renderer.Renderer; import com.jme3.renderer.queue.RenderQueue.Bucket; import com.jme3.scene.Geometry; -import com.jme3.shader.Shader; -import com.jme3.shader.Uniform; -import com.jme3.shader.UniformBindingManager; -import com.jme3.shader.VarType; +import com.jme3.shader.*; import com.jme3.texture.Image; import com.jme3.texture.Texture; import com.jme3.texture.image.ColorSpace; @@ -412,6 +409,17 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { return paramValues.get(name); } + /** + * Returns the current parameter's value. + * + * @param name the parameter name to look up. + * @return current value or null if the parameter wasn't set. + */ + public T getParamValue(final String name) { + final MatParam param = paramValues.get(name); + return param == null ? null : (T) param.getValue(); + } + /** * Returns the texture parameter set on this material with the given name, * returns null if the parameter is not set. @@ -660,6 +668,28 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { setParam(name, VarType.Vector4, value); } + /** + * Pass an uniform buffer object to the material shader. + * + * @param name the name of the buffer object defined in the material definition (j3md). + * @param value the buffer object. + */ + public void setUniformBufferObject(final String name, final BufferObject value) { + value.setBufferType(BufferObject.BufferType.UniformBufferObject); + setParam(name, VarType.BufferObject, value); + } + + /** + * Pass a shader storage buffer object to the material shader. + * + * @param name the name of the buffer object defined in the material definition (j3md). + * @param value the buffer object. + */ + public void setShaderStorageBufferObject(final String name, final BufferObject value) { + value.setBufferType(BufferObject.BufferType.ShaderStorageBufferObject); + setParam(name, VarType.BufferObject, value); + } + /** * Pass a Vector2f to the material shader. * @@ -794,20 +824,29 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { } for (int i = 0; i < paramValues.size(); i++) { + MatParam param = paramValues.getValue(i); VarType type = param.getVarType(); - Uniform uniform = shader.getUniform(param.getPrefixedName()); - if (uniform.isSetByCurrentMaterial()) { - continue; - } + if (isBO(type)) { + + final ShaderBufferBlock bufferBlock = shader.getBufferBlock(param.getPrefixedName()); + bufferBlock.setBufferObject((BufferObject) param.getValue()); - if (type.isTextureType()) { - renderer.setTexture(unit, (Texture) param.getValue()); - uniform.setValue(VarType.Int, unit); - unit++; } else { - uniform.setValue(type, param.getValue()); + + Uniform uniform = shader.getUniform(param.getPrefixedName()); + if (uniform.isSetByCurrentMaterial()) { + continue; + } + + if (type.isTextureType()) { + renderer.setTexture(unit, (Texture) param.getValue()); + uniform.setValue(VarType.Int, unit); + unit++; + } else { + uniform.setValue(type, param.getValue()); + } } } @@ -815,6 +854,16 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { return unit; } + /** + * Returns true if the type is Buffer Object's type. + * + * @param type the material parameter type. + * @return true if the type is Buffer Object's type. + */ + private boolean isBO(final VarType type) { + return type == VarType.BufferObject; + } + private void updateRenderState(RenderManager renderManager, Renderer renderer, TechniqueDef techniqueDef) { if (renderManager.getForcedRenderState() != null) { renderer.applyRenderState(renderManager.getForcedRenderState()); diff --git a/jme3-core/src/main/java/com/jme3/renderer/Caps.java b/jme3-core/src/main/java/com/jme3/renderer/Caps.java index d8e73a36e..0369517e2 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Caps.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Caps.java @@ -394,7 +394,15 @@ public enum Caps { /** * GPU can provide and accept binary shaders. */ - BinaryShader; + BinaryShader, + /** + * Supporting working with UniformBufferObject. + */ + UniformBufferObject, + /** + * Supporting working with ShaderStorageBufferObjects. + */ + ShaderStorageBufferObject; /** * Returns true if given the renderer capabilities, the texture 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 a7e737092..cd1bfec35 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Limits.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Limits.java @@ -62,4 +62,20 @@ public enum Limits { ColorTextureSamples, DepthTextureSamples, TextureAnisotropy, + + // UBO + UniformBufferObjectMaxVertexBlocks, + UniformBufferObjectMaxFragmentBlocks, + UniformBufferObjectMaxGeometryBlocks, + UniformBufferObjectMaxBlockSize, + + // SSBO + ShaderStorageBufferObjectMaxBlockSize, + ShaderStorageBufferObjectMaxVertexBlocks, + ShaderStorageBufferObjectMaxFragmentBlocks, + ShaderStorageBufferObjectMaxGeometryBlocks, + ShaderStorageBufferObjectMaxTessControlBlocks, + ShaderStorageBufferObjectMaxTessEvaluationBlocks, + ShaderStorageBufferObjectMaxComputeBlocks, + ShaderStorageBufferObjectMaxCombineBlocks, } diff --git a/jme3-core/src/main/java/com/jme3/renderer/Renderer.java b/jme3-core/src/main/java/com/jme3/renderer/Renderer.java index 9f562ea61..201729da8 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Renderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Renderer.java @@ -35,6 +35,7 @@ import com.jme3.material.RenderState; import com.jme3.math.ColorRGBA; import com.jme3.scene.Mesh; import com.jme3.scene.VertexBuffer; +import com.jme3.shader.BufferObject; import com.jme3.shader.Shader; import com.jme3.shader.Shader.ShaderSource; import com.jme3.system.AppSettings; @@ -267,12 +268,26 @@ public interface Renderer { */ public void updateBufferData(VertexBuffer vb); + /** + * Uploads data of the buffer object on the GPU. + * + * @param bo the buffer object to upload. + */ + public void updateBufferData(BufferObject bo); + /** * Deletes a vertex buffer from the GPU. * @param vb The vertex buffer to delete */ public void deleteBuffer(VertexBuffer vb); + /** + * Deletes the buffer object from the GPU. + * + * @param bo the buffer object to delete. + */ + public void deleteBuffer(BufferObject bo); + /** * Renders count meshes, with the geometry data supplied and * per-instance data supplied. 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 e0a1975c1..b2a57736f 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 @@ -45,156 +45,157 @@ import java.nio.ShortBuffer; */ public interface GL { - static final int GL_ALPHA = 0x1906; - static final int GL_ALWAYS = 0x207; - static final int GL_ARRAY_BUFFER = 0x8892; - static final int GL_BACK = 0x405; - static final int GL_BLEND = 0xBE2; - static final int GL_BLUE = 0x1905; - static final int GL_BYTE = 0x1400; - static final int GL_CLAMP_TO_EDGE = 0x812F; - static final int GL_COLOR_BUFFER_BIT = 0x4000; - static final int GL_COMPILE_STATUS = 0x8B81; - static final int GL_CULL_FACE = 0xB44; - static final int GL_DECR = 0x1E03; - static final int GL_DECR_WRAP = 0x8508; - static final int GL_DEPTH_BUFFER_BIT = 0x100; - static final int GL_DEPTH_COMPONENT = 0x1902; - static final int GL_DEPTH_COMPONENT16 = 0x81A5; - static final int GL_DEPTH_TEST = 0xB71; - static final int GL_DOUBLE = 0x140A; - static final int GL_DST_ALPHA = 0x0304; - static final int GL_DST_COLOR = 0x306; - static final int GL_DYNAMIC_DRAW = 0x88E8; - static final int GL_ELEMENT_ARRAY_BUFFER = 0x8893; - static final int GL_EQUAL = 0x202; - static final int GL_EXTENSIONS = 0x1F03; - static final int GL_FALSE = 0x0; - static final int GL_FLOAT = 0x1406; - static final int GL_FRAGMENT_SHADER = 0x8B30; - static final int GL_FRONT = 0x404; - static final int GL_FUNC_ADD = 0x8006; - static final int GL_FUNC_SUBTRACT = 0x800A; - static final int GL_FUNC_REVERSE_SUBTRACT = 0x800B; - static final int GL_FRONT_AND_BACK = 0x408; - static final int GL_GEQUAL = 0x206; - static final int GL_GREATER = 0x204; - static final int GL_GREEN = 0x1904; - static final int GL_INCR = 0x1E02; - static final int GL_INCR_WRAP = 0x8507; - static final int GL_INFO_LOG_LENGTH = 0x8B84; - static final int GL_INT = 0x1404; - static final int GL_INVALID_ENUM = 0x500; - static final int GL_INVALID_VALUE = 0x501; - static final int GL_INVALID_OPERATION = 0x502; - static final int GL_INVERT = 0x150A; - static final int GL_KEEP = 0x1E00; - static final int GL_LEQUAL = 0x203; - static final int GL_LESS = 0x201; - static final int GL_LINEAR = 0x2601; - static final int GL_LINEAR_MIPMAP_LINEAR = 0x2703; - static final int GL_LINEAR_MIPMAP_NEAREST = 0x2701; - static final int GL_LINES = 0x1; - static final int GL_LINE_LOOP = 0x2; - static final int GL_LINE_STRIP = 0x3; - static final int GL_LINK_STATUS = 0x8B82; - static final int GL_LUMINANCE = 0x1909; - static final int GL_LUMINANCE_ALPHA = 0x190A; - static final int GL_MAX = 0x8008; - static final int GL_MAX_CUBE_MAP_TEXTURE_SIZE = 0x851C; - static final int GL_MAX_FRAGMENT_UNIFORM_COMPONENTS = 0x8B49; - static final int GL_MAX_FRAGMENT_UNIFORM_VECTORS = 0x8DFD; - static final int GL_MAX_TEXTURE_IMAGE_UNITS = 0x8872; - static final int GL_MAX_TEXTURE_SIZE = 0xD33; - static final int GL_MAX_VERTEX_ATTRIBS = 0x8869; - static final int GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS = 0x8B4C; - static final int GL_MAX_VERTEX_UNIFORM_COMPONENTS = 0x8B4A; - static final int GL_MAX_VERTEX_UNIFORM_VECTORS = 0x8DFB; - static final int GL_MIRRORED_REPEAT = 0x8370; - static final int GL_MIN = 0x8007; - static final int GL_NEAREST = 0x2600; - static final int GL_NEAREST_MIPMAP_LINEAR = 0x2702; - static final int GL_NEAREST_MIPMAP_NEAREST = 0x2700; - static final int GL_NEVER = 0x200; - static final int GL_NO_ERROR = 0x0; - static final int GL_NONE = 0x0; - static final int GL_NOTEQUAL = 0x205; - static final int GL_ONE = 0x1; - static final int GL_ONE_MINUS_DST_ALPHA = 0x0305; - static final int GL_ONE_MINUS_DST_COLOR = 0x307; - static final int GL_ONE_MINUS_SRC_ALPHA = 0x303; - static final int GL_ONE_MINUS_SRC_COLOR = 0x301; - static final int GL_OUT_OF_MEMORY = 0x505; - static final int GL_POINTS = 0x0; - static final int GL_POLYGON_OFFSET_FILL = 0x8037; - static final int GL_QUERY_RESULT = 0x8866; - static final int GL_QUERY_RESULT_AVAILABLE = 0x8867; - static final int GL_RED = 0x1903; - static final int GL_RENDERER = 0x1F01; - static final int GL_REPEAT = 0x2901; - static final int GL_REPLACE = 0x1E01; - static final int GL_RGB = 0x1907; - static final int GL_RGB565 = 0x8D62; - static final int GL_RGB5_A1 = 0x8057; - static final int GL_RGBA = 0x1908; - static final int GL_RGBA4 = 0x8056; - static final int GL_SCISSOR_TEST = 0xC11; - static final int GL_SHADING_LANGUAGE_VERSION = 0x8B8C; - static final int GL_SHORT = 0x1402; - static final int GL_SRC_ALPHA = 0x302; - static final int GL_SRC_ALPHA_SATURATE = 0x0308; - static final int GL_SRC_COLOR = 0x300; - static final int GL_STATIC_DRAW = 0x88E4; - static final int GL_STENCIL_BUFFER_BIT = 0x400; - static final int GL_STENCIL_TEST = 0xB90; - static final int GL_STREAM_DRAW = 0x88E0; - static final int GL_STREAM_READ = 0x88E1; - static final int GL_TEXTURE = 0x1702; - static final int GL_TEXTURE0 = 0x84C0; - static final int GL_TEXTURE1 = 0x84C1; - static final int GL_TEXTURE2 = 0x84C2; - static final int GL_TEXTURE3 = 0x84C3; - static final int GL_TEXTURE4 = 0x84C4; - static final int GL_TEXTURE5 = 0x84C5; - static final int GL_TEXTURE6 = 0x84C6; - static final int GL_TEXTURE7 = 0x84C7; - static final int GL_TEXTURE8 = 0x84C8; - static final int GL_TEXTURE9 = 0x84C9; - static final int GL_TEXTURE10 = 0x84CA; - static final int GL_TEXTURE11 = 0x84CB; - static final int GL_TEXTURE12 = 0x84CC; - static final int GL_TEXTURE13 = 0x84CD; - static final int GL_TEXTURE14 = 0x84CE; - static final int GL_TEXTURE15 = 0x84CF; - static final int GL_TEXTURE_2D = 0xDE1; - static final int GL_TEXTURE_CUBE_MAP = 0x8513; - static final int GL_TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515; - static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516; - static final int GL_TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517; - static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518; - static final int GL_TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519; - static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A; - static final int GL_TEXTURE_MAG_FILTER = 0x2800; - static final int GL_TEXTURE_MIN_FILTER = 0x2801; - static final int GL_TEXTURE_WRAP_S = 0x2802; - static final int GL_TEXTURE_WRAP_T = 0x2803; - static final int GL_TIME_ELAPSED = 0x88BF; - static final int GL_TRIANGLES = 0x4; - static final int GL_TRIANGLE_FAN = 0x6; - static final int GL_TRIANGLE_STRIP = 0x5; - static final int GL_TRUE = 0x1; - static final int GL_UNPACK_ALIGNMENT = 0xCF5; - static final int GL_UNSIGNED_BYTE = 0x1401; - static final int GL_UNSIGNED_INT = 0x1405; - static final int GL_UNSIGNED_SHORT = 0x1403; - static final int GL_UNSIGNED_SHORT_5_6_5 = 0x8363; - static final int GL_UNSIGNED_SHORT_5_5_5_1 = 0x8034; - static final int GL_VENDOR = 0x1F00; - static final int GL_VERSION = 0x1F02; - static final int GL_VERTEX_SHADER = 0x8B31; - static final int GL_ZERO = 0x0; - - void resetStats(); + public static final int GL_ALPHA = 0x1906; + public static final int GL_ALWAYS = 0x207; + public static final int GL_ARRAY_BUFFER = 0x8892; + public static final int GL_BACK = 0x405; + public static final int GL_BLEND = 0xBE2; + public static final int GL_BLUE = 0x1905; + public static final int GL_BYTE = 0x1400; + public static final int GL_CLAMP_TO_EDGE = 0x812F; + public static final int GL_COLOR_BUFFER_BIT = 0x4000; + public static final int GL_COMPILE_STATUS = 0x8B81; + public static final int GL_CULL_FACE = 0xB44; + public static final int GL_DECR = 0x1E03; + public static final int GL_DECR_WRAP = 0x8508; + public static final int GL_DEPTH_BUFFER_BIT = 0x100; + public static final int GL_DEPTH_COMPONENT = 0x1902; + public static final int GL_DEPTH_COMPONENT16 = 0x81A5; + public static final int GL_DEPTH_TEST = 0xB71; + public static final int GL_DOUBLE = 0x140A; + public static final int GL_DST_ALPHA = 0x0304; + public static final int GL_DST_COLOR = 0x306; + public static final int GL_DYNAMIC_DRAW = 0x88E8; + public static final int GL_DYNAMIC_COPY = 0x88EA; + public static final int GL_ELEMENT_ARRAY_BUFFER = 0x8893; + public static final int GL_EQUAL = 0x202; + public static final int GL_EXTENSIONS = 0x1F03; + public static final int GL_FALSE = 0x0; + public static final int GL_FLOAT = 0x1406; + public static final int GL_FRAGMENT_SHADER = 0x8B30; + public static final int GL_FRONT = 0x404; + public static final int GL_FUNC_ADD = 0x8006; + public static final int GL_FUNC_SUBTRACT = 0x800A; + public static final int GL_FUNC_REVERSE_SUBTRACT = 0x800B; + public static final int GL_FRONT_AND_BACK = 0x408; + public static final int GL_GEQUAL = 0x206; + public static final int GL_GREATER = 0x204; + public static final int GL_GREEN = 0x1904; + public static final int GL_INCR = 0x1E02; + public static final int GL_INCR_WRAP = 0x8507; + public static final int GL_INFO_LOG_LENGTH = 0x8B84; + public static final int GL_INT = 0x1404; + public static final int GL_INVALID_ENUM = 0x500; + public static final int GL_INVALID_VALUE = 0x501; + public static final int GL_INVALID_OPERATION = 0x502; + public static final int GL_INVERT = 0x150A; + public static final int GL_KEEP = 0x1E00; + public static final int GL_LEQUAL = 0x203; + public static final int GL_LESS = 0x201; + public static final int GL_LINEAR = 0x2601; + public static final int GL_LINEAR_MIPMAP_LINEAR = 0x2703; + public static final int GL_LINEAR_MIPMAP_NEAREST = 0x2701; + public static final int GL_LINES = 0x1; + public static final int GL_LINE_LOOP = 0x2; + public static final int GL_LINE_STRIP = 0x3; + public static final int GL_LINK_STATUS = 0x8B82; + public static final int GL_LUMINANCE = 0x1909; + public static final int GL_LUMINANCE_ALPHA = 0x190A; + public static final int GL_MAX = 0x8008; + public static final int GL_MAX_CUBE_MAP_TEXTURE_SIZE = 0x851C; + public static final int GL_MAX_FRAGMENT_UNIFORM_COMPONENTS = 0x8B49; + public static final int GL_MAX_FRAGMENT_UNIFORM_VECTORS = 0x8DFD; + public static final int GL_MAX_TEXTURE_IMAGE_UNITS = 0x8872; + public static final int GL_MAX_TEXTURE_SIZE = 0xD33; + public static final int GL_MAX_VERTEX_ATTRIBS = 0x8869; + public static final int GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS = 0x8B4C; + public static final int GL_MAX_VERTEX_UNIFORM_COMPONENTS = 0x8B4A; + public static final int GL_MAX_VERTEX_UNIFORM_VECTORS = 0x8DFB; + public static final int GL_MIRRORED_REPEAT = 0x8370; + public static final int GL_MIN = 0x8007; + public static final int GL_NEAREST = 0x2600; + public static final int GL_NEAREST_MIPMAP_LINEAR = 0x2702; + public static final int GL_NEAREST_MIPMAP_NEAREST = 0x2700; + public static final int GL_NEVER = 0x200; + public static final int GL_NO_ERROR = 0x0; + public static final int GL_NONE = 0x0; + public static final int GL_NOTEQUAL = 0x205; + public static final int GL_ONE = 0x1; + public static final int GL_ONE_MINUS_DST_ALPHA = 0x0305; + public static final int GL_ONE_MINUS_DST_COLOR = 0x307; + public static final int GL_ONE_MINUS_SRC_ALPHA = 0x303; + public static final int GL_ONE_MINUS_SRC_COLOR = 0x301; + public static final int GL_OUT_OF_MEMORY = 0x505; + public static final int GL_POINTS = 0x0; + public static final int GL_POLYGON_OFFSET_FILL = 0x8037; + public static final int GL_QUERY_RESULT = 0x8866; + public static final int GL_QUERY_RESULT_AVAILABLE = 0x8867; + public static final int GL_RED = 0x1903; + public static final int GL_RENDERER = 0x1F01; + public static final int GL_REPEAT = 0x2901; + public static final int GL_REPLACE = 0x1E01; + public static final int GL_RGB = 0x1907; + public static final int GL_RGB565 = 0x8D62; + public static final int GL_RGB5_A1 = 0x8057; + public static final int GL_RGBA = 0x1908; + public static final int GL_RGBA4 = 0x8056; + public static final int GL_SCISSOR_TEST = 0xC11; + public static final int GL_SHADING_LANGUAGE_VERSION = 0x8B8C; + public static final int GL_SHORT = 0x1402; + public static final int GL_SRC_ALPHA = 0x302; + public static final int GL_SRC_ALPHA_SATURATE = 0x0308; + public static final int GL_SRC_COLOR = 0x300; + public static final int GL_STATIC_DRAW = 0x88E4; + public static final int GL_STENCIL_BUFFER_BIT = 0x400; + public static final int GL_STENCIL_TEST = 0xB90; + public static final int GL_STREAM_DRAW = 0x88E0; + public static final int GL_STREAM_READ = 0x88E1; + public static final int GL_TEXTURE = 0x1702; + public static final int GL_TEXTURE0 = 0x84C0; + public static final int GL_TEXTURE1 = 0x84C1; + public static final int GL_TEXTURE2 = 0x84C2; + public static final int GL_TEXTURE3 = 0x84C3; + public static final int GL_TEXTURE4 = 0x84C4; + public static final int GL_TEXTURE5 = 0x84C5; + public static final int GL_TEXTURE6 = 0x84C6; + public static final int GL_TEXTURE7 = 0x84C7; + public static final int GL_TEXTURE8 = 0x84C8; + public static final int GL_TEXTURE9 = 0x84C9; + public static final int GL_TEXTURE10 = 0x84CA; + public static final int GL_TEXTURE11 = 0x84CB; + public static final int GL_TEXTURE12 = 0x84CC; + public static final int GL_TEXTURE13 = 0x84CD; + public static final int GL_TEXTURE14 = 0x84CE; + public static final int GL_TEXTURE15 = 0x84CF; + public static final int GL_TEXTURE_2D = 0xDE1; + public static final int GL_TEXTURE_CUBE_MAP = 0x8513; + public static final int GL_TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515; + public static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516; + public static final int GL_TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517; + 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_MAG_FILTER = 0x2800; + public static final int GL_TEXTURE_MIN_FILTER = 0x2801; + public static final int GL_TEXTURE_WRAP_S = 0x2802; + public static final int GL_TEXTURE_WRAP_T = 0x2803; + public static final int GL_TIME_ELAPSED = 0x88BF; + public static final int GL_TRIANGLES = 0x4; + public static final int GL_TRIANGLE_FAN = 0x6; + public static final int GL_TRIANGLE_STRIP = 0x5; + public static final int GL_TRUE = 0x1; + public static final int GL_UNPACK_ALIGNMENT = 0xCF5; + public static final int GL_UNSIGNED_BYTE = 0x1401; + public static final int GL_UNSIGNED_INT = 0x1405; + public static final int GL_UNSIGNED_SHORT = 0x1403; + public static final int GL_UNSIGNED_SHORT_5_6_5 = 0x8363; + public static final int GL_UNSIGNED_SHORT_5_5_5_1 = 0x8034; + public static final int GL_VENDOR = 0x1F00; + public static final int GL_VERSION = 0x1F02; + public static final int GL_VERTEX_SHADER = 0x8B31; + public static final int GL_ZERO = 0x0; + + public void resetStats(); /** *

Reference Page

@@ -204,7 +205,7 @@ public interface GL { * * @param texture which texture unit to make active. One of:
{@link #GL_TEXTURE0 TEXTURE0}GL_TEXTURE[1-31]
*/ - void glActiveTexture(int texture); + public void glActiveTexture(int texture); /** *

Reference Page

@@ -225,7 +226,7 @@ public interface GL { * @param program the program object to which a shader object will be attached. * @param shader the shader object that is to be attached. */ - void glAttachShader(int program, int shader); + public void glAttachShader(int program, int shader); /** *

Reference Page

@@ -235,7 +236,7 @@ public interface GL { * @param target the target type of query object established. * @param query the name of a query object. */ - void glBeginQuery(int target, int query); + public void glBeginQuery(int target, int query); /** *

Reference Page

@@ -245,7 +246,7 @@ public interface GL { * @param target the target to which the buffer object is bound. * @param buffer the name of a buffer object. */ - void glBindBuffer(int target, int buffer); + public void glBindBuffer(int target, int buffer); /** *

Reference Page

@@ -259,7 +260,7 @@ public interface GL { * @param target the texture target. * @param texture the texture object to bind. */ - void glBindTexture(int target, int texture); + public void glBindTexture(int target, int texture); /** *

Reference Page

@@ -269,7 +270,7 @@ public interface GL { * @param colorMode the RGB blend equation, how the red, green, and blue components of the source and destination colors are combined. * @param alphaMode the alpha blend equation, how the alpha component of the source and destination colors are combined */ - void glBlendEquationSeparate(int colorMode, int alphaMode); + public void glBlendEquationSeparate(int colorMode, int alphaMode); /** *

Reference Page

@@ -279,7 +280,7 @@ public interface GL { * @param sfactor the source weighting factor. * @param dfactor the destination weighting factor. */ - void glBlendFunc(int sfactor, int dfactor); + public void glBlendFunc(int sfactor, int dfactor); /** *

Reference Page

@@ -291,7 +292,7 @@ public interface GL { * @param sfactorAlpha how the alpha source blending factor is computed. The initial value is GL_ONE. * @param dfactorAlpha how the alpha destination blending factor is computed. The initial value is GL_ZERO. */ - void glBlendFuncSeparate(int sfactorRGB, int dfactorRGB, int sfactorAlpha, int dfactorAlpha); + public void glBlendFuncSeparate(int sfactorRGB, int dfactorRGB, int sfactorAlpha, int dfactorAlpha); /** *

Reference Page

@@ -321,7 +322,7 @@ public interface GL { * @param dataSize the size in bytes of the buffer object's new data store * @param usage the expected usage pattern of the data store. */ - void glBufferData(int target, long dataSize, int usage); + public void glBufferData(int target, long dataSize, int usage); /** *

Reference Page

@@ -351,7 +352,7 @@ public interface GL { * @param data a pointer to data that will be copied into the data store for initialization, or {@code NULL} if no data is to be copied. * @param usage the expected usage pattern of the data store. */ - void glBufferData(int target, FloatBuffer data, int usage); + public void glBufferData(int target, FloatBuffer data, int usage); /** *

Reference Page

@@ -381,7 +382,7 @@ public interface GL { * @param data a pointer to data that will be copied into the data store for initialization, or {@code NULL} if no data is to be copied * @param usage the expected usage pattern of the data store. */ - void glBufferData(int target, ShortBuffer data, int usage); + public void glBufferData(int target, ShortBuffer data, int usage); /** *

Reference Page

@@ -411,7 +412,7 @@ public interface GL { * @param data a pointer to data that will be copied into the data store for initialization, or {@code NULL} if no data is to be copied. * @param usage the expected usage pattern of the data store. */ - void glBufferData(int target, ByteBuffer data, int usage); + public void glBufferData(int target, ByteBuffer data, int usage); /** *

Reference Page

@@ -422,7 +423,7 @@ public interface GL { * @param offset the offset into the buffer object's data store where data replacement will begin, measured in bytes. * @param data a pointer to the new data that will be copied into the data store. */ - void glBufferSubData(int target, long offset, FloatBuffer data); + public void glBufferSubData(int target, long offset, FloatBuffer data); /** *

Reference Page

@@ -433,7 +434,7 @@ public interface GL { * @param offset the offset into the buffer object's data store where data replacement will begin, measured in bytes. * @param data a pointer to the new data that will be copied into the data store. */ - void glBufferSubData(int target, long offset, ShortBuffer data); + public void glBufferSubData(int target, long offset, ShortBuffer data); /** *

Reference Page

@@ -444,7 +445,7 @@ public interface GL { * @param offset the offset into the buffer object's data store where data replacement will begin, measured in bytes. * @param data a pointer to the new data that will be copied into the data store. */ - void glBufferSubData(int target, long offset, ByteBuffer data); + public void glBufferSubData(int target, long offset, ByteBuffer data); /** *

Reference Page

@@ -454,7 +455,7 @@ public interface GL { * * @param mask Zero or the bitwise OR of one or more values indicating which buffers are to be cleared. */ - void glClear(int mask); + public void glClear(int mask); /** *

Reference Page

@@ -466,7 +467,7 @@ public interface GL { * @param blue the value to which to clear the B channel of the color buffer. * @param alpha the value to which to clear the A channel of the color buffer. */ - void glClearColor(float red, float green, float blue, float alpha); + public void glClearColor(float red, float green, float blue, float alpha); /** *

Reference Page

@@ -478,7 +479,7 @@ public interface GL { * @param blue whether B values are written or not. * @param alpha whether A values are written or not. */ - void glColorMask(boolean red, boolean green, boolean blue, boolean alpha); + public void glColorMask(boolean red, boolean green, boolean blue, boolean alpha); /** *

Reference Page

@@ -487,7 +488,7 @@ public interface GL { * * @param shader the shader object to be compiled. */ - void glCompileShader(int shader); + public void glCompileShader(int shader); /** *

Reference Page

@@ -502,7 +503,7 @@ public interface GL { * @param border must be 0 * @param data a pointer to the compressed image data */ - void glCompressedTexImage2D(int target, int level, int internalFormat, int width, int height, int border, + public void glCompressedTexImage2D(int target, int level, int internalFormat, int width, int height, int border, ByteBuffer data); /** @@ -519,7 +520,7 @@ public interface GL { * @param format the format of the compressed image data stored at address {@code data}. * @param data a pointer to the compressed image data. */ - void glCompressedTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, + public void glCompressedTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, ByteBuffer data); /** @@ -527,7 +528,7 @@ public interface GL { *

* Creates a program object. */ - int glCreateProgram(); + public int glCreateProgram(); /** *

Reference Page

@@ -536,7 +537,7 @@ public interface GL { * * @param shaderType the type of shader to be created. One of:
{@link #GL_VERTEX_SHADER VERTEX_SHADER}{@link #GL_FRAGMENT_SHADER FRAGMENT_SHADER}{@link GL3#GL_GEOMETRY_SHADER GEOMETRY_SHADER}{@link GL4#GL_TESS_CONTROL_SHADER TESS_CONTROL_SHADER}
{@link GL4#GL_TESS_EVALUATION_SHADER TESS_EVALUATION_SHADER}
*/ - int glCreateShader(int shaderType); + public int glCreateShader(int shaderType); /** *

Reference Page

@@ -547,7 +548,7 @@ public interface GL { * * @param mode the CullFace mode. One of:
{@link #GL_FRONT FRONT}{@link #GL_BACK BACK}{@link #GL_FRONT_AND_BACK FRONT_AND_BACK}
*/ - void glCullFace(int mode); + public void glCullFace(int mode); /** *

Reference Page

@@ -556,7 +557,7 @@ public interface GL { * * @param buffers an array of buffer objects to be deleted. */ - void glDeleteBuffers(IntBuffer buffers); + public void glDeleteBuffers(IntBuffer buffers); /** *

Reference Page

@@ -565,7 +566,7 @@ public interface GL { * * @param program the program object to be deleted. */ - void glDeleteProgram(int program); + public void glDeleteProgram(int program); /** *

Reference Page

@@ -574,7 +575,7 @@ public interface GL { * * @param shader the shader object to be deleted. */ - void glDeleteShader(int shader); + public void glDeleteShader(int shader); /** *

Reference Page

@@ -589,7 +590,7 @@ public interface GL { * * @param textures contains {@code n} names of texture objects to be deleted. */ - void glDeleteTextures(IntBuffer textures); + public void glDeleteTextures(IntBuffer textures); /** *

Reference Page

@@ -598,7 +599,7 @@ public interface GL { * * @param func the depth test comparison. One of:
{@link #GL_NEVER NEVER}{@link #GL_ALWAYS ALWAYS}{@link #GL_LESS LESS}{@link #GL_LEQUAL LEQUAL}{@link #GL_EQUAL EQUAL}{@link #GL_GREATER GREATER}{@link #GL_GEQUAL GEQUAL}{@link #GL_NOTEQUAL NOTEQUAL}
*/ - void glDepthFunc(int func); + public void glDepthFunc(int func); /** *

Reference Page

@@ -607,7 +608,7 @@ public interface GL { * * @param flag whether depth values are written or not. */ - void glDepthMask(boolean flag); + public void glDepthMask(boolean flag); /** *

Reference Page

@@ -617,7 +618,7 @@ public interface GL { * @param nearVal the near depth range. * @param farVal the far depth range. */ - void glDepthRange(double nearVal, double farVal); + public void glDepthRange(double nearVal, double farVal); /** *

Reference Page

@@ -627,7 +628,7 @@ public interface GL { * @param program the program object from which to detach the shader object. * @param shader the shader object to be detached. */ - void glDetachShader(int program, int shader); + public void glDetachShader(int program, int shader); /** *

Reference Page

@@ -636,7 +637,7 @@ public interface GL { * * @param cap the OpenGL state to disable. */ - void glDisable(int cap); + public void glDisable(int cap); /** *

Reference Page

@@ -645,7 +646,7 @@ public interface GL { * * @param index the index of the generic vertex attribute to be disabled. */ - void glDisableVertexAttribArray(int index); + public void glDisableVertexAttribArray(int index); /** *

Reference Page

@@ -660,7 +661,7 @@ public interface GL { * @param first the first vertex to transfer to the GL. * @param count the number of vertices after {@code first} to transfer to the GL. */ - void glDrawArrays(int mode, int first, int count); + public void glDrawArrays(int mode, int first, int count); /** *

Reference Page

@@ -700,7 +701,7 @@ public interface GL { * @param type the type of the values in {@code indices}. * @param indices a pointer to the location where the indices are stored. */ - void glDrawRangeElements(int mode, int start, int end, int count, int type, long indices); /// GL2+ + public void glDrawRangeElements(int mode, int start, int end, int count, int type, long indices); /// GL2+ /** *

Reference Page

@@ -709,7 +710,7 @@ public interface GL { * * @param cap the OpenGL state to enable. */ - void glEnable(int cap); + public void glEnable(int cap); /** *

Reference Page

@@ -718,7 +719,7 @@ public interface GL { * * @param index the index of the generic vertex attribute to be enabled. */ - void glEnableVertexAttribArray(int index); + public void glEnableVertexAttribArray(int index); /** *

Reference Page

@@ -727,7 +728,7 @@ public interface GL { * * @param target the query object target. */ - void glEndQuery(int target); + public void glEndQuery(int target); /** *

Reference Page

@@ -736,7 +737,7 @@ public interface GL { * * @param buffers a buffer in which the generated buffer object names are stored. */ - void glGenBuffers(IntBuffer buffers); + public void glGenBuffers(IntBuffer buffers); /** *

Reference Page

@@ -746,7 +747,7 @@ public interface GL { * * @param textures a scalar or buffer in which to place the returned texture names. */ - void glGenTextures(IntBuffer textures); + public void glGenTextures(IntBuffer textures); /** *

Reference Page

@@ -755,7 +756,7 @@ public interface GL { * * @param ids a buffer in which the generated query object names are stored. */ - void glGenQueries(int number, IntBuffer ids); + public void glGenQueries(int number, IntBuffer ids); /** *

Reference Page

@@ -765,7 +766,7 @@ public interface GL { * @param program the program object to be queried. * @param name a null terminated string containing the name of the attribute variable whose location is to be queried. */ - int glGetAttribLocation(int program, String name); + public int glGetAttribLocation(int program, String name); /** *

Reference Page

@@ -779,7 +780,7 @@ public interface GL { * @param pname the state variable. * @param params a scalar or buffer in which to place the returned data. */ - void glGetBoolean(int pname, ByteBuffer params); + public void glGetBoolean(int pname, ByteBuffer params); /** *

Reference Page

@@ -790,7 +791,7 @@ public interface GL { * @param offset the offset into the buffer object's data store from which data will be returned, measured in bytes. * @param data a pointer to the location where buffer object data is returned. */ - void glGetBufferSubData(int target, long offset, ByteBuffer data); + public void glGetBufferSubData(int target, long offset, ByteBuffer data); /** *

Reference Page

@@ -800,7 +801,7 @@ public interface GL { * further error will again record its code. If a call to {@code GetError} returns {@link #GL_NO_ERROR NO_ERROR}, then there has been no detectable error since * the last call to {@code GetError} (or since the GL was initialized). */ - int glGetError(); + public int glGetError(); /** *

Reference Page

@@ -814,7 +815,7 @@ public interface GL { * @param pname the state variable. * @param params a scalar or buffer in which to place the returned data. */ - void glGetInteger(int pname, IntBuffer params); + public void glGetInteger(int pname, IntBuffer params); /** *

Reference Page

@@ -825,7 +826,7 @@ public interface GL { * @param pname the object parameter. * @param params the requested object parameter. */ - void glGetProgram(int program, int pname, IntBuffer params); + public void glGetProgram(int program, int pname, IntBuffer params); /** *

Reference Page

@@ -835,7 +836,7 @@ public interface GL { * @param program the program object whose information log is to be queried. * @param maxSize the size of the character buffer for storing the returned information log. */ - String glGetProgramInfoLog(int program, int maxSize); + public String glGetProgramInfoLog(int program, int maxSize); /** * Unsigned version. @@ -843,7 +844,7 @@ public interface GL { * @param query the name of a query object * @param pname the symbolic name of a query object parameter */ - long glGetQueryObjectui64(int query, int pname); + public long glGetQueryObjectui64(int query, int pname); /** *

Reference Page

@@ -853,7 +854,7 @@ public interface GL { * @param query the name of a query object * @param pname the symbolic name of a query object parameter. One of:
{@link #GL_QUERY_RESULT QUERY_RESULT}{@link #GL_QUERY_RESULT_AVAILABLE QUERY_RESULT_AVAILABLE}
*/ - int glGetQueryObjectiv(int query, int pname); + public int glGetQueryObjectiv(int query, int pname); /** *

Reference Page

@@ -864,7 +865,7 @@ public interface GL { * @param pname the object parameter. * @param params the requested object parameter. */ - void glGetShader(int shader, int pname, IntBuffer params); + public void glGetShader(int shader, int pname, IntBuffer params); /** *

Reference Page

@@ -874,7 +875,7 @@ public interface GL { * @param shader the shader object whose information log is to be queried. * @param maxSize the size of the character buffer for storing the returned information log. */ - String glGetShaderInfoLog(int shader, int maxSize); + public String glGetShaderInfoLog(int shader, int maxSize); /** *

Reference Page

@@ -883,7 +884,7 @@ public interface GL { * * @param name the property to query. One of:
{@link #GL_RENDERER RENDERER}{@link #GL_VENDOR VENDOR}{@link #GL_EXTENSIONS EXTENSIONS}{@link #GL_VERSION VERSION}{@link GL2#GL_SHADING_LANGUAGE_VERSION SHADING_LANGUAGE_VERSION}
*/ - String glGetString(int name); + public String glGetString(int name); /** *

Reference Page

@@ -893,7 +894,7 @@ public interface GL { * @param program the program object to be queried. * @param name a null terminated string containing the name of the uniform variable whose location is to be queried. */ - int glGetUniformLocation(int program, String name); + public int glGetUniformLocation(int program, String name); /** *

Reference Page

@@ -902,7 +903,7 @@ public interface GL { * * @param cap the enable state to query. */ - boolean glIsEnabled(int cap); + public boolean glIsEnabled(int cap); /** *

Reference Page

@@ -911,7 +912,7 @@ public interface GL { * * @param width the line width. */ - void glLineWidth(float width); + public void glLineWidth(float width); /** *

Reference Page

@@ -920,7 +921,7 @@ public interface GL { * * @param program the program object to be linked. */ - void glLinkProgram(int program); + public void glLinkProgram(int program); /** *

Reference Page

@@ -930,7 +931,7 @@ public interface GL { * @param pname the pixel store parameter to set. * @param param the parameter value */ - void glPixelStorei(int pname, int param); + public void glPixelStorei(int pname, int param); /** *

Reference Page

@@ -944,7 +945,7 @@ public interface GL { * @param factor the maximum depth slope factor. * @param units the constant scale. */ - void glPolygonOffset(float factor, float units); + public void glPolygonOffset(float factor, float units); /** *

Reference Page

@@ -963,7 +964,7 @@ public interface GL { * @param type the pixel type. * @param data a buffer in which to place the returned pixel data. */ - void glReadPixels(int x, int y, int width, int height, int format, int type, ByteBuffer data); + public void glReadPixels(int x, int y, int width, int height, int format, int type, ByteBuffer data); /** @@ -983,7 +984,7 @@ public interface GL { * @param type the pixel type. * @param offset a buffer in which to place the returned pixel data/ */ - void glReadPixels(int x, int y, int width, int height, int format, int type, long offset); + public void glReadPixels(int x, int y, int width, int height, int format, int type, long offset); /** *

Reference Page

@@ -998,7 +999,7 @@ public interface GL { * @param width the scissor rectangle width. * @param height the scissor rectangle height. */ - void glScissor(int x, int y, int width, int height); + public void glScissor(int x, int y, int width, int height); /** *

Reference Page

@@ -1013,7 +1014,7 @@ public interface GL { * @param shader the shader object whose source code is to be replaced, * @param strings an array of pointers to strings containing the source code to be loaded into the shader */ - void glShaderSource(int shader, String[] strings, IntBuffer length); + public void glShaderSource(int shader, String[] strings, IntBuffer length); /** *

Reference Page

@@ -1026,7 +1027,7 @@ public interface GL { * buffer. The initial value is 0. * @param mask a mask that is ANDed with both the reference value and the stored stencil value when the test is done. The initial value is all 1's. */ - void glStencilFuncSeparate(int face, int func, int ref, int mask); + public void glStencilFuncSeparate(int face, int func, int ref, int mask); /** *

Reference Page

@@ -1039,7 +1040,7 @@ public interface GL { * @param dppass the stencil action when both the stencil test and the depth test pass, or when the stencil test passes and either there is no depth buffer or depth * testing is not enabled. The initial value is GL_KEEP. */ - void glStencilOpSeparate(int face, int sfail, int dpfail, int dppass); + public void glStencilOpSeparate(int face, int sfail, int dpfail, int dppass); /** *

Reference Page

@@ -1056,7 +1057,7 @@ public interface GL { * @param type the texel data type. * @param data the texel data. */ - void glTexImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, + public void glTexImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, ByteBuffer data); /** @@ -1068,7 +1069,7 @@ public interface GL { * @param pname the parameter to set. * @param param the parameter value. */ - void glTexParameterf(int target, int pname, float param); + public void glTexParameterf(int target, int pname, float param); /** *

Reference Page

@@ -1079,7 +1080,7 @@ public interface GL { * @param pname the parameter to set. * @param param the parameter value. */ - void glTexParameteri(int target, int pname, int param); + public void glTexParameteri(int target, int pname, int param); /** *

Reference Page

@@ -1097,7 +1098,7 @@ public interface GL { * @param type the pixel data type. * @param data the pixel data. */ - void glTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, + public void glTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer data); /** @@ -1108,7 +1109,7 @@ public interface GL { * @param location the location of the uniform variable to be modified. * @param value a pointer to an array of {@code count} values that will be used to update the specified uniform variable. */ - void glUniform1(int location, FloatBuffer value); + public void glUniform1(int location, FloatBuffer value); /** *

Reference Page

@@ -1118,7 +1119,7 @@ public interface GL { * @param location the location of the uniform variable to be modified. * @param value a pointer to an array of {@code count} values that will be used to update the specified uniform variable. */ - void glUniform1(int location, IntBuffer value); + public void glUniform1(int location, IntBuffer value); /** *

Reference Page

@@ -1128,7 +1129,7 @@ public interface GL { * @param location the location of the uniform variable to be modified. * @param v0 the uniform value. */ - void glUniform1f(int location, float v0); + public void glUniform1f(int location, float v0); /** *

Reference Page

@@ -1138,7 +1139,7 @@ public interface GL { * @param location the location of the uniform variable to be modified. * @param v0 the uniform value. */ - void glUniform1i(int location, int v0); + public void glUniform1i(int location, int v0); /** *

Reference Page

@@ -1148,7 +1149,7 @@ public interface GL { * @param location the location of the uniform variable to be modified. * @param value a pointer to an array of {@code count} values that will be used to update the specified uniform variable. */ - void glUniform2(int location, IntBuffer value); + public void glUniform2(int location, IntBuffer value); /** *

Reference Page

@@ -1158,7 +1159,7 @@ public interface GL { * @param location the location of the uniform variable to be modified. * @param value a pointer to an array of {@code count} values that will be used to update the specified uniform variable. */ - void glUniform2(int location, FloatBuffer value); + public void glUniform2(int location, FloatBuffer value); /** *

Reference Page

@@ -1169,7 +1170,7 @@ public interface GL { * @param v0 the uniform x value. * @param v1 the uniform y value. */ - void glUniform2f(int location, float v0, float v1); + public void glUniform2f(int location, float v0, float v1); /** *

Reference Page

@@ -1179,7 +1180,7 @@ public interface GL { * @param location the location of the uniform variable to be modified. * @param value a pointer to an array of {@code count} values that will be used to update the specified uniform variable. */ - void glUniform3(int location, IntBuffer value); + public void glUniform3(int location, IntBuffer value); /** *

Reference Page

@@ -1189,7 +1190,7 @@ public interface GL { * @param location the location of the uniform variable to be modified. * @param value a pointer to an array of {@code count} values that will be used to update the specified uniform variable. */ - void glUniform3(int location, FloatBuffer value); + public void glUniform3(int location, FloatBuffer value); /** *

Reference Page

@@ -1201,7 +1202,7 @@ public interface GL { * @param v1 the uniform y value. * @param v2 the uniform z value. */ - void glUniform3f(int location, float v0, float v1, float v2); + public void glUniform3f(int location, float v0, float v1, float v2); /** *

Reference Page

@@ -1211,7 +1212,7 @@ public interface GL { * @param location the location of the uniform variable to be modified. * @param value a pointer to an array of {@code count} values that will be used to update the specified uniform variable. */ - void glUniform4(int location, FloatBuffer value); + public void glUniform4(int location, FloatBuffer value); /** *

Reference Page

@@ -1221,7 +1222,7 @@ public interface GL { * @param location the location of the uniform variable to be modified. * @param value a pointer to an array of {@code count} values that will be used to update the specified uniform variable. */ - void glUniform4(int location, IntBuffer value); + public void glUniform4(int location, IntBuffer value); /** *

Reference Page

@@ -1234,7 +1235,7 @@ public interface GL { * @param v2 the uniform z value. * @param v3 the uniform w value. */ - void glUniform4f(int location, float v0, float v1, float v2, float v3); + public void glUniform4f(int location, float v0, float v1, float v2, float v3); /** *

Reference Page

@@ -1245,7 +1246,7 @@ public interface GL { * @param transpose whether to transpose the matrix as the values are loaded into the uniform variable. * @param value a pointer to an array of {@code count} values that will be used to update the specified uniform variable. */ - void glUniformMatrix3(int location, boolean transpose, FloatBuffer value); + public void glUniformMatrix3(int location, boolean transpose, FloatBuffer value); /** *

Reference Page

@@ -1256,7 +1257,7 @@ public interface GL { * @param transpose whether to transpose the matrix as the values are loaded into the uniform variable. * @param value a pointer to an array of {@code count} values that will be used to update the specified uniform variable. */ - void glUniformMatrix4(int location, boolean transpose, FloatBuffer value); + public void glUniformMatrix4(int location, boolean transpose, FloatBuffer value); /** *

Reference Page

@@ -1265,7 +1266,7 @@ public interface GL { * * @param program the program object whose executables are to be used as part of current rendering state. */ - void glUseProgram(int program); + public void glUseProgram(int program); /** *

Reference Page

@@ -1281,7 +1282,7 @@ public interface GL { * @param pointer the vertex attribute data or the offset of the first component of the first generic vertex attribute in the array in the data store of the buffer * currently bound to the {@link GL#GL_ARRAY_BUFFER ARRAY_BUFFER} target. The initial value is 0. */ - void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, long pointer); + public void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, long pointer); /** *

Reference Page

@@ -1297,5 +1298,5 @@ public interface GL { * @param width the viewport width. * @param height the viewport height. */ - void glViewport(int x, int y, int width, int height); -} + public void glViewport(int x, int y, int width, int height); +} \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java index 595f5ff85..dcf1d91eb 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java @@ -83,6 +83,46 @@ public interface GL3 extends GL2 { public static final int GL_RGB_INTEGER = 36248; public static final int GL_RGBA_INTEGER = 36249; + public static final int GL_UNIFORM_OFFSET = 0x8A3B; + + /** + * Accepted by the {@code target} parameters of BindBuffer, BufferData, BufferSubData, MapBuffer, UnmapBuffer, GetBufferSubData, and GetBufferPointerv. + */ + public static final int GL_UNIFORM_BUFFER = 0x8A11; + + /** + * Accepted by the {@code pname} parameter of GetActiveUniformBlockiv. + */ + public static final int GL_UNIFORM_BLOCK_BINDING = 0x8A3F; + public static final int GL_UNIFORM_BLOCK_DATA_SIZE = 0x8A40; + public static final int GL_UNIFORM_BLOCK_NAME_LENGTH = 0x8A41; + public static final int GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS = 0x8A42; + public static final int GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES = 0x8A43; + public static final int GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = 0x8A44; + public static final int GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER = 0x8A45; + public static final int GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = 0x8A46; + + /** + * Accepted by the <pname> parameter of GetBooleanv, GetIntegerv, + * GetFloatv, and GetDoublev: + */ + public static final int GL_MAX_VERTEX_UNIFORM_BLOCKS = 0x8A2B; + public static final int GL_MAX_GEOMETRY_UNIFORM_BLOCKS = 0x8A2C; + public static final int GL_MAX_FRAGMENT_UNIFORM_BLOCKS = 0x8A2D; + public static final int GL_MAX_COMBINED_UNIFORM_BLOCKS = 0x8A2E; + public static final int GL_MAX_UNIFORM_BUFFER_BINDINGS = 0x8A2F; + public static final int GL_MAX_UNIFORM_BLOCK_SIZE = 0x8A30; + public static final int GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS = 0x8A31; + public static final int GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS = 0x8A32; + public static final int GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS = 0x8A33; + public static final int GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT = 0x8A34; + + /** + * Accepted by the {@code target} parameters of BindBuffer, BufferData, BufferSubData, MapBuffer, UnmapBuffer, GetBufferSubData, GetBufferPointerv, + * BindBufferRange, BindBufferOffset and BindBufferBase. + */ + public static final int GL_TRANSFORM_FEEDBACK_BUFFER = 0x8C8E; + /** *

Reference Page

*

@@ -128,4 +168,47 @@ public interface GL3 extends GL2 { * @param index the index of the particular element being queried. */ public String glGetString(int name, int index); /// GL3+ + + + /** + *

Reference Page

+ * + * Retrieves the index of a named uniform block. + * + * @param program the name of a program containing the uniform block. + * @param uniformBlockName an array of characters to containing the name of the uniform block whose index to retrieve. + * @return the block index. + */ + public int glGetUniformBlockIndex(int program, String uniformBlockName); + + /** + *

Reference Page

+ * + * Binds a buffer object to an indexed buffer target. + * + * @param target the target of the bind operation. One of:
{@link #GL_TRANSFORM_FEEDBACK_BUFFER TRANSFORM_FEEDBACK_BUFFER}{@link #GL_UNIFORM_BUFFER UNIFORM_BUFFER}{@link GL4#GL_ATOMIC_COUNTER_BUFFER ATOMIC_COUNTER_BUFFER}{@link GL4#GL_SHADER_STORAGE_BUFFER SHADER_STORAGE_BUFFER}
+ * @param index the index of the binding point within the array specified by {@code target} + * @param buffer a buffer object to bind to the specified binding point + */ + public void glBindBufferBase(int target, int index, int buffer); + + /** + * Binding points for active uniform blocks are assigned using glUniformBlockBinding. Each of a program's active + * uniform blocks has a corresponding uniform buffer binding point. program is the name of a program object for + * which the command glLinkProgram has been issued in the past. + *

+ * If successful, glUniformBlockBinding specifies that program will use the data store of the buffer object bound + * to the binding point uniformBlockBinding to extract the values of the uniforms in the uniform block identified + * by uniformBlockIndex. + *

+ * When a program object is linked or re-linked, the uniform buffer object binding point assigned to each of its + * active uniform blocks is reset to zero. + * + * @param program The name of a program object containing the active uniform block whose binding to + * assign. + * @param uniformBlockIndex The index of the active uniform block within program whose binding to assign. + * @param uniformBlockBinding Specifies the binding point to which to bind the uniform block with index + * uniformBlockIndex within program. + */ + public void glUniformBlockBinding(int program, int uniformBlockIndex, int uniformBlockBinding); } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java index 9afe4755b..821959aee 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java @@ -42,6 +42,32 @@ public interface GL4 extends GL3 { public static final int GL_TESS_EVALUATION_SHADER = 0x8E87; public static final int GL_PATCHES = 0xE; + /** + * Accepted by the {@code target} parameter of BindBufferBase and BindBufferRange. + */ + public static final int GL_ATOMIC_COUNTER_BUFFER = 0x92C0; + + /** + * Accepted by the {@code target} parameters of BindBuffer, BufferData, BufferSubData, MapBuffer, UnmapBuffer, GetBufferSubData, and GetBufferPointerv. + */ + public static final int GL_SHADER_STORAGE_BUFFER = 0x90D2; + public static final int GL_SHADER_STORAGE_BLOCK = 0x92E6; + + /** + * Accepted by the <pname> parameter of GetIntegerv, GetBooleanv, + * GetInteger64v, GetFloatv, and GetDoublev: + */ + public static final int GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = 0x90D6; + public static final int GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS = 0x90D7; + public static final int GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS = 0x90D8; + public static final int GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS = 0x90D9; + public static final int GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS = 0x90DA; + public static final int GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS = 0x90DB; + public static final int GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS = 0x90DC; + public static final int GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS = 0x90DD; + public static final int GL_MAX_SHADER_STORAGE_BLOCK_SIZE = 0x90DE; + public static final int GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT = 0x90DF; + /** *

Reference Page

*

@@ -50,4 +76,28 @@ public interface GL4 extends GL3 { * @param count the new value for the parameter given by {@code pname} */ public void glPatchParameter(int count); + + /** + * Returns the unsigned integer index assigned to a resource named name in the interface type programInterface of + * program object program. + * + * @param program the name of a program object whose resources to query. + * @param programInterface a token identifying the interface within program containing the resource named name. + * @param name the name of the resource to query the index of. + * @return the index of a named resource within a program. + */ + public int glGetProgramResourceIndex(int program, int programInterface, String name); + + /** + * Cchanges the active shader storage block with an assigned index of storageBlockIndex in program object program. + * storageBlockIndex must be an active shader storage block index in program. storageBlockBinding must be less + * than the value of {@code #GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS}. If successful, glShaderStorageBlockBinding specifies + * that program will use the data store of the buffer object bound to the binding point storageBlockBinding to + * read and write the values of the buffer variables in the shader storage block identified by storageBlockIndex. + * + * @param program the name of a program object whose resources to query. + * @param storageBlockIndex The index storage block within the program. + * @param storageBlockBinding The index storage block binding to associate with the specified storage block. + */ + public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding); } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java index a945a1d85..3dfd2a8ca 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java @@ -83,6 +83,19 @@ public class GLDebugDesktop extends GLDebugES implements GL2, GL3, GL4 { return result; } + @Override + public int glGetUniformBlockIndex(final int program, final String uniformBlockName) { + final int result = gl3.glGetUniformBlockIndex(program, uniformBlockName); + checkError(); + return result; + } + + @Override + public void glBindBufferBase(final int target, final int index, final int buffer) { + gl3.glBindBufferBase(target, index, buffer); + checkError(); + } + @Override public void glDeleteVertexArrays(IntBuffer arrays) { gl3.glDeleteVertexArrays(arrays); @@ -95,8 +108,27 @@ public class GLDebugDesktop extends GLDebugES implements GL2, GL3, GL4 { checkError(); } + @Override + public int glGetProgramResourceIndex(int program, int programInterface, String name) { + final int result = gl4.glGetProgramResourceIndex(program, programInterface, name); + checkError(); + return result; + } + + @Override + public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { + gl4.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + checkError(); + } + public void glBlendEquationSeparate(int colorMode, int alphaMode) { gl.glBlendEquationSeparate(colorMode, alphaMode); checkError(); } + + @Override + public void glUniformBlockBinding(final int program, final int uniformBlockIndex, final int uniformBlockBinding) { + gl3.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); + checkError(); + } } 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 746db4084..790e538f8 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 @@ -45,11 +45,9 @@ import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer.Format; 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.*; import com.jme3.shader.Shader.ShaderSource; import com.jme3.shader.Shader.ShaderType; -import com.jme3.shader.Uniform; import com.jme3.texture.FrameBuffer; import com.jme3.texture.FrameBuffer.RenderBuffer; import com.jme3.texture.Image; @@ -61,17 +59,17 @@ import com.jme3.util.BufferUtils; import com.jme3.util.ListMap; import com.jme3.util.MipMapGenerator; import com.jme3.util.NativeObjectManager; -import java.nio.*; -import java.util.Arrays; -import java.util.EnumMap; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.List; +import jme3tools.shader.ShaderDebug; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import jme3tools.shader.ShaderDebug; public final class GLRenderer implements Renderer { @@ -480,6 +478,26 @@ public final class GLRenderer implements Renderer { } } + if (hasExtension("GL_ARB_shader_storage_buffer_object")) { + caps.add(Caps.ShaderStorageBufferObject); + limits.put(Limits.ShaderStorageBufferObjectMaxBlockSize, getInteger(GL4.GL_MAX_SHADER_STORAGE_BLOCK_SIZE)); + limits.put(Limits.ShaderStorageBufferObjectMaxComputeBlocks, getInteger(GL4.GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS)); + limits.put(Limits.ShaderStorageBufferObjectMaxGeometryBlocks, getInteger(GL4.GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS)); + limits.put(Limits.ShaderStorageBufferObjectMaxFragmentBlocks, getInteger(GL4.GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS)); + limits.put(Limits.ShaderStorageBufferObjectMaxVertexBlocks, getInteger(GL4.GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS)); + limits.put(Limits.ShaderStorageBufferObjectMaxTessControlBlocks, getInteger(GL4.GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS)); + limits.put(Limits.ShaderStorageBufferObjectMaxTessEvaluationBlocks, getInteger(GL4.GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS)); + limits.put(Limits.ShaderStorageBufferObjectMaxCombineBlocks, getInteger(GL4.GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS)); + } + + if (hasExtension("GL_ARB_uniform_buffer_object")) { + caps.add(Caps.UniformBufferObject); + limits.put(Limits.UniformBufferObjectMaxBlockSize, getInteger(GL3.GL_MAX_UNIFORM_BLOCK_SIZE)); + limits.put(Limits.UniformBufferObjectMaxGeometryBlocks, getInteger(GL3.GL_MAX_GEOMETRY_UNIFORM_BLOCKS)); + limits.put(Limits.UniformBufferObjectMaxFragmentBlocks, getInteger(GL3.GL_MAX_FRAGMENT_UNIFORM_BLOCKS)); + limits.put(Limits.UniformBufferObjectMaxVertexBlocks, getInteger(GL3.GL_MAX_VERTEX_UNIFORM_BLOCKS)); + } + // Print context information logger.log(Level.INFO, "OpenGL Renderer Information\n" + " * Vendor: {0}\n" + @@ -1050,12 +1068,25 @@ public final class GLRenderer implements Renderer { } } + @Override public void postFrame() { objManager.deleteUnused(this); OpenCLObjectManager.getInstance().deleteUnusedObjects(); gl.resetStats(); } + protected void bindProgram(Shader shader) { + int shaderId = shader.getId(); + if (context.boundShaderProgram != shaderId) { + gl.glUseProgram(shaderId); + statistics.onShaderUse(shader, true); + context.boundShader = shader; + context.boundShaderProgram = shaderId; + } else { + statistics.onShaderUse(shader, false); + } + } + /*********************************************************************\ |* Shaders *| \*********************************************************************/ @@ -1070,18 +1101,6 @@ public final class GLRenderer implements Renderer { } } - protected void bindProgram(Shader shader) { - int shaderId = shader.getId(); - if (context.boundShaderProgram != shaderId) { - gl.glUseProgram(shaderId); - statistics.onShaderUse(shader, true); - context.boundShader = shader; - context.boundShaderProgram = shaderId; - } else { - statistics.onShaderUse(shader, false); - } - } - protected void updateUniform(Shader shader, Uniform uniform) { int shaderId = shader.getId(); @@ -1187,6 +1206,58 @@ public final class GLRenderer implements Renderer { } } + /** + * Updates the buffer block for the shader. + * + * @param shader the shader. + * @param bufferBlock the storage block. + */ + protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBlock bufferBlock) { + + assert bufferBlock.getName() != null; + assert shader.getId() > 0; + + final BufferObject bufferObject = bufferBlock.getBufferObject(); + if (bufferObject.getUniqueId() == -1 || bufferObject.isUpdateNeeded()) { + updateBufferData(bufferObject); + } + + if (!bufferBlock.isUpdateNeeded()) { + return; + } + + bindProgram(shader); + + final int shaderId = shader.getId(); + final BufferObject.BufferType bufferType = bufferObject.getBufferType(); + + bindBuffer(bufferBlock, bufferObject, shaderId, bufferType); + + bufferBlock.clearUpdateNeeded(); + } + + private void bindBuffer(final ShaderBufferBlock bufferBlock, final BufferObject bufferObject, final int shaderId, + final BufferObject.BufferType bufferType) { + + switch (bufferType) { + case UniformBufferObject: { + final int blockIndex = gl3.glGetUniformBlockIndex(shaderId, bufferBlock.getName()); + gl3.glBindBufferBase(GL3.GL_UNIFORM_BUFFER, bufferObject.getBinding(), bufferObject.getId()); + gl3.glUniformBlockBinding(GL3.GL_UNIFORM_BUFFER, blockIndex, bufferObject.getBinding()); + break; + } + case ShaderStorageBufferObject: { + final int blockIndex = gl4.glGetProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName()); + gl4.glShaderStorageBlockBinding(shaderId, blockIndex, bufferObject.getBinding()); + gl4.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bufferObject.getBinding(), bufferObject.getId()); + break; + } + default: { + throw new IllegalArgumentException("Doesn't support binding of " + bufferType); + } + } + } + protected void updateShaderUniforms(Shader shader) { ListMap uniforms = shader.getUniformMap(); for (int i = 0; i < uniforms.size(); i++) { @@ -1197,6 +1268,18 @@ public final class GLRenderer implements Renderer { } } + /** + * Updates all shader's buffer blocks. + * + * @param shader the shader. + */ + protected void updateShaderBufferBlocks(final Shader shader) { + final ListMap bufferBlocks = shader.getBufferBlockMap(); + for (int i = 0; i < bufferBlocks.size(); i++) { + updateShaderBufferBlock(shader, bufferBlocks.getValue(i)); + } + } + protected void resetUniformLocations(Shader shader) { ListMap uniforms = shader.getUniformMap(); for (int i = 0; i < uniforms.size(); i++) { @@ -1415,6 +1498,7 @@ public final class GLRenderer implements Renderer { assert shader.getId() > 0; updateShaderUniforms(shader); + updateShaderBufferBlocks(shader); bindProgram(shader); } } @@ -2503,6 +2587,58 @@ public final class GLRenderer implements Renderer { vb.clearUpdateNeeded(); } + @Override + public void updateBufferData(final BufferObject bo) { + + int maxSize = Integer.MAX_VALUE; + + final BufferObject.BufferType bufferType = bo.getBufferType(); + + if (!caps.contains(bufferType.getRequiredCaps())) { + throw new IllegalArgumentException("The current video hardware doesn't support " + bufferType); + } + + final ByteBuffer data = bo.computeData(maxSize); + if (data == null) { + throw new IllegalArgumentException("Can't upload BO without data."); + } + + int bufferId = bo.getId(); + if (bufferId == -1) { + + // create buffer + intBuf1.clear(); + gl.glGenBuffers(intBuf1); + bufferId = intBuf1.get(0); + + bo.setId(bufferId); + + objManager.registerObject(bo); + } + + data.rewind(); + + switch (bufferType) { + case UniformBufferObject: { + gl3.glBindBuffer(GL3.GL_UNIFORM_BUFFER, bufferId); + gl3.glBufferData(GL4.GL_UNIFORM_BUFFER, data, GL3.GL_DYNAMIC_DRAW); + gl3.glBindBuffer(GL4.GL_UNIFORM_BUFFER, 0); + break; + } + case ShaderStorageBufferObject: { + gl4.glBindBuffer(GL4.GL_SHADER_STORAGE_BUFFER, bufferId); + gl4.glBufferData(GL4.GL_SHADER_STORAGE_BUFFER, data, GL4.GL_DYNAMIC_COPY); + gl4.glBindBuffer(GL4.GL_SHADER_STORAGE_BUFFER, 0); + break; + } + default: { + throw new IllegalArgumentException("Doesn't support binding of " + bufferType); + } + } + + bo.clearUpdateNeeded(); + } + public void deleteBuffer(VertexBuffer vb) { int bufId = vb.getId(); if (bufId != -1) { @@ -2516,6 +2652,23 @@ public final class GLRenderer implements Renderer { } } + @Override + public void deleteBuffer(final BufferObject bo) { + + int bufferId = bo.getId(); + if (bufferId == -1) { + return; + } + + intBuf1.clear(); + intBuf1.put(bufferId); + intBuf1.flip(); + + gl.glDeleteBuffers(intBuf1); + + bo.resetObject(); + } + public void clearVertexAttribs() { IDList attribList = context.attribIndexList; for (int i = 0; i < attribList.oldLen; i++) { diff --git a/jme3-core/src/main/java/com/jme3/shader/BufferObject.java b/jme3-core/src/main/java/com/jme3/shader/BufferObject.java new file mode 100644 index 000000000..886f4a1ab --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/shader/BufferObject.java @@ -0,0 +1,828 @@ +package com.jme3.shader; + +import com.jme3.math.*; +import com.jme3.renderer.Caps; +import com.jme3.renderer.Renderer; +import com.jme3.util.BufferUtils; +import com.jme3.util.NativeObject; +import com.jme3.util.SafeArrayList; + +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The base implementation of BO. + * + * @author JavaSaBr + */ +public class BufferObject extends NativeObject { + + private static final Map, VarType> CLASS_TO_VAR_TYPE = new HashMap<>(); + + static { + CLASS_TO_VAR_TYPE.put(Float.class, VarType.Float); + CLASS_TO_VAR_TYPE.put(Integer.class, VarType.Int); + CLASS_TO_VAR_TYPE.put(Boolean.class, VarType.Boolean); + CLASS_TO_VAR_TYPE.put(Vector2f.class, VarType.Vector2); + CLASS_TO_VAR_TYPE.put(Vector3f.class, VarType.Vector3); + CLASS_TO_VAR_TYPE.put(ColorRGBA.class, VarType.Vector4); + CLASS_TO_VAR_TYPE.put(Quaternion.class, VarType.Vector4); + CLASS_TO_VAR_TYPE.put(Vector4f.class, VarType.Vector4); + + CLASS_TO_VAR_TYPE.put(Vector2f[].class, VarType.Vector2Array); + CLASS_TO_VAR_TYPE.put(Vector3f[].class, VarType.Vector3Array); + CLASS_TO_VAR_TYPE.put(Vector4f[].class, VarType.Vector4Array); + CLASS_TO_VAR_TYPE.put(ColorRGBA[].class, VarType.Vector4Array); + CLASS_TO_VAR_TYPE.put(Quaternion[].class, VarType.Vector4Array); + + CLASS_TO_VAR_TYPE.put(Matrix3f.class, VarType.Matrix3); + CLASS_TO_VAR_TYPE.put(Matrix4f.class, VarType.Matrix4); + CLASS_TO_VAR_TYPE.put(Matrix3f[].class, VarType.Matrix3Array); + CLASS_TO_VAR_TYPE.put(Matrix4f[].class, VarType.Matrix4Array); + } + + protected static VarType getVarTypeByValue(final Object value) { + + final VarType varType = CLASS_TO_VAR_TYPE.get(value.getClass()); + if (varType != null) { + return varType; + } else if (value instanceof Collection && ((Collection) value).isEmpty()) { + throw new IllegalArgumentException("Can't calculate a var type for the empty collection value[" + value + "]."); + } else if (value instanceof List) { + return getVarTypeByValue(((List) value).get(0)); + } else if (value instanceof Collection) { + return getVarTypeByValue(((Collection) value).iterator().next()); + } + + throw new IllegalArgumentException("Can't calculate a var type for the value " + value); + } + + public enum Layout { + std140, + /** unsupported yet */ + @Deprecated + std430, + } + + public enum BufferType { + ShaderStorageBufferObject(Caps.ShaderStorageBufferObject), + UniformBufferObject(Caps.UniformBufferObject), + ; + + private final Caps requiredCaps; + + BufferType(final Caps requiredCaps) { + this.requiredCaps = requiredCaps; + } + + /** + * Get the required caps. + * + * @return the required caps. + */ + public Caps getRequiredCaps() { + return requiredCaps; + } + } + + /** + * The fields of this BO. + */ + private final Map fields; + + /** + * The field's array. + */ + private final SafeArrayList fieldArray; + + /** + * The buffer's data layout. + */ + private final Layout layout; + + /** + * The binding number. + */ + private final int binding; + + /** + * The buffer's type. + */ + private BufferType bufferType; + + /** + * The previous data buffer. + */ + private ByteBuffer previousData; + + public BufferObject(final int binding, final Layout layout, final BufferType bufferType) { + this.handleRef = new Object(); + this.bufferType = bufferType; + this.binding = binding; + this.layout = layout; + this.fields = new HashMap<>(); + this.fieldArray = new SafeArrayList<>(BufferObjectField.class); + } + + public BufferObject(final int binding, final Layout layout) { + this(binding, layout, BufferType.UniformBufferObject); + } + + public BufferObject(final int binding, final BufferType bufferType) { + this(binding, Layout.std140, bufferType); + } + + public BufferObject(final BufferType bufferType) { + this(1, Layout.std140, bufferType); + } + + public BufferObject(final Layout layout) { + this(1, layout, BufferType.UniformBufferObject); + } + + public BufferObject(final int binding) { + this(binding, Layout.std140, BufferType.UniformBufferObject); + } + + public BufferObject() { + this(1, Layout.std140, BufferType.UniformBufferObject); + } + + private BufferObject(final Void unused, final int id) { + super(id); + this.fieldArray = null; + this.fields = null; + this.layout = null; + this.binding = 0; + } + + /** + * Declares a filed in this BO. + * + * @param name the field's name. + * @param varType the field's type. + */ + public void declareField(final String name, final VarType varType) { + + if (fields.containsKey(name)) { + throw new IllegalArgumentException("The field " + name + " is already declared."); + } + + final BufferObjectField field = new BufferObjectField(name, varType); + + fields.put(name, field); + fieldArray.add(field); + } + + /** + * Gets the buffer's type. + * + * @return the buffer's type. + */ + public BufferType getBufferType() { + return bufferType; + } + + /** + * Sets the buffer's type. + * + * @param bufferType the buffer's type. + */ + public void setBufferType(final BufferType bufferType) { + + if (getId() != -1) { + throw new IllegalStateException("Can't change buffer's type when this buffer is already initialized."); + } + + this.bufferType = bufferType; + } + + /** + * Sets the value to the filed by the field's name. + * + * @param name the field's name. + * @param value the value. + */ + public void setFieldValue(final String name, final Object value) { + + BufferObjectField field = fields.get(name); + + if (field == null) { + declareField(name, getVarTypeByValue(value)); + field = fields.get(name); + } + + field.setValue(value); + setUpdateNeeded(); + } + + /** + * Gets the current value of the field by the name. + * + * @param name the field name. + * @param the value's type. + * @return the current value. + */ + public T getFieldValue(final String name) { + + final BufferObjectField field = fields.get(name); + if (field == null) { + throw new IllegalArgumentException("Unknown a field with the name " + name); + } + + return (T) field.getValue(); + } + + /** + * Get the binding number. + * + * @return the binding number. + */ + public int getBinding() { + return binding; + } + + @Override + public void resetObject() { + this.id = -1; + setUpdateNeeded(); + } + + /** + * Computes the current binary data of this BO. + * + * @param maxSize the max data size. + * @return the current binary data of this BO. + */ + public ByteBuffer computeData(final int maxSize) { + + int estimateSize = 0; + + for (final BufferObjectField field : fieldArray) { + estimateSize += estimateSize(field); + } + + if(maxSize < estimateSize) { + throw new IllegalStateException("The estimated size(" + estimateSize + ") of this BO is bigger than " + + "maximum available size " + maxSize); + } + + if (previousData != null) { + if (previousData.capacity() < estimateSize) { + BufferUtils.destroyDirectBuffer(previousData); + previousData = null; + } else { + previousData.clear(); + } + } + + final ByteBuffer data = previousData == null ? BufferUtils.createByteBuffer(estimateSize) : previousData; + + for (final BufferObjectField field : fieldArray) { + writeField(field, data); + } + + data.flip(); + + this.previousData = data; + + return data; + } + + /** + * Estimates size of the field. + * + * @param field the field. + * @return the estimated size. + */ + protected int estimateSize(final BufferObjectField field) { + + switch (field.getType()) { + case Float: + case Int: { + if (layout == Layout.std140) { + return 16; + } + return 4; + } + case Boolean: { + if (layout == Layout.std140) { + return 16; + } + return 1; + } + case Vector2: { + return 4 * 2; + } + case Vector3: { + final int multiplier = layout == Layout.std140 ? 4 : 3; + return 4 * multiplier; + } + case Vector4: + return 16; + case IntArray: { + return estimate((int[]) field.getValue()); + } + case FloatArray: { + return estimate((float[]) field.getValue()); + } + case Vector2Array: { + return estimateArray(field.getValue(), 8); + } + case Vector3Array: { + final int multiplier = layout == Layout.std140 ? 16 : 12; + return estimateArray(field.getValue(), multiplier); + } + case Vector4Array: { + return estimateArray(field.getValue(), 16); + } + case Matrix3: { + final int multiplier = layout == Layout.std140 ? 16 : 12; + return 3 * 3 * multiplier; + } + case Matrix4: { + return 4 * 4 * 4; + } + case Matrix3Array: { + int multiplier = layout == Layout.std140 ? 16 : 12; + multiplier = 3 * 3 * multiplier; + return estimateArray(field.getValue(), multiplier); + } + case Matrix4Array: { + final int multiplier = 4 * 4 * 16; + return estimateArray(field.getValue(), multiplier); + } + default: { + throw new IllegalArgumentException("The type of BO field " + field.getType() + " doesn't support."); + } + } + } + + /** + * Estimates bytes count to present the value on GPU. + * + * @param value the value. + * @param multiplier the multiplier. + * @return the estimated bytes cunt. + */ + protected int estimateArray(final Object value, final int multiplier) { + + if (value instanceof Object[]) { + return ((Object[]) value).length * multiplier; + } else if (value instanceof Collection) { + return ((Collection) value).size() * multiplier; + } + + throw new IllegalArgumentException("Unexpected value " + value); + } + + /** + * Estimates bytes count to present the values on GPU. + * + * @param values the values. + * @return the estimated bytes cunt. + */ + protected int estimate(final float[] values) { + return values.length * 4; + } + + /** + * Estimates bytes count to present the values on GPU. + * + * @param values the values. + * @return the estimated bytes cunt. + */ + protected int estimate(final int[] values) { + return values.length * 4; + } + + /** + * Writes the field to the data buffer. + * + * @param field the field. + * @param data the data buffer. + */ + protected void writeField(final BufferObjectField field, final ByteBuffer data) { + + final Object value = field.getValue(); + + switch (field.getType()) { + case Int: { + data.putInt(((Number) value).intValue()); + if (layout == Layout.std140) { + data.putInt(0); + data.putLong(0); + } + break; + } + case Float: { + data.putFloat(((Number) value).floatValue()); + if (layout == Layout.std140) { + data.putInt(0); + data.putLong(0); + } + break; + } + case Boolean: + data.put((byte) (((Boolean) value) ? 1 : 0)); + if (layout == Layout.std140) { + data.putInt(0); + data.putLong(0); + data.putShort((short) 0); + data.put((byte) 0); + } + break; + case Vector2: + write(data, (Vector2f) value); + break; + case Vector3: + write(data, (Vector3f) value); + break; + case Vector4: + writeVec4(data, value); + break; + case IntArray: { + write(data, (int[]) value); + break; + } + case FloatArray: { + write(data, (float[]) value); + break; + } + case Vector2Array: { + writeVec2Array(data, value); + break; + } + case Vector3Array: { + writeVec3Array(data, value); + break; + } + case Vector4Array: { + writeVec4Array(data, value); + break; + } + case Matrix3: { + write(data, (Matrix3f) value); + break; + } + case Matrix4: { + write(data, (Matrix4f) value); + break; + } + case Matrix3Array: { + writeMat3Array(data, value); + break; + } + case Matrix4Array: { + writeMat4Array(data, value); + break; + } + default: { + throw new IllegalArgumentException("The type of BO field " + field.getType() + " doesn't support."); + } + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void writeMat3Array(final ByteBuffer data, final Object value) { + + if (value instanceof Matrix3f[]) { + + final Matrix3f[] values = (Matrix3f[]) value; + for (final Matrix3f mat : values) { + write(data, mat); + } + + } else if(value instanceof SafeArrayList) { + + final SafeArrayList values = (SafeArrayList) value; + for (final Matrix3f mat : values.getArray()) { + write(data, mat); + } + + } else if(value instanceof Collection) { + + final Collection values = (Collection) value; + for (final Matrix3f mat : values) { + write(data, mat); + } + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void writeMat4Array(final ByteBuffer data, final Object value) { + + if (value instanceof Matrix4f[]) { + + final Matrix4f[] values = (Matrix4f[]) value; + for (final Matrix4f mat : values) { + write(data, mat); + } + + } else if(value instanceof SafeArrayList) { + + final SafeArrayList values = (SafeArrayList) value; + for (final Matrix4f mat : values.getArray()) { + write(data, mat); + } + + } else if(value instanceof Collection) { + + final Collection values = (Collection) value; + for (final Matrix4f mat : values) { + write(data, mat); + } + } + } + + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void writeVec4Array(final ByteBuffer data, final Object value) { + + if (value instanceof Object[]) { + + final Object[] values = (Object[]) value; + for (final Object vec : values) { + writeVec4(data, vec); + } + + } else if(value instanceof SafeArrayList) { + + final SafeArrayList values = (SafeArrayList) value; + for (final Object vec : values.getArray()) { + writeVec4(data, vec); + } + + } else if(value instanceof Collection) { + + final Collection values = (Collection) value; + for (final Object vec : values) { + writeVec4(data, vec); + } + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void writeVec3Array(final ByteBuffer data, final Object value) { + + if (value instanceof Vector3f[]) { + + final Vector3f[] values = (Vector3f[]) value; + for (final Vector3f vec : values) { + write(data, vec); + } + + } else if(value instanceof SafeArrayList) { + + final SafeArrayList values = (SafeArrayList) value; + for (final Vector3f vec : values.getArray()) { + write(data, vec); + } + + } else if(value instanceof Collection) { + + final Collection values = (Collection) value; + for (final Vector3f vec : values) { + write(data, vec); + } + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void writeVec2Array(final ByteBuffer data, final Object value) { + + if (value instanceof Vector2f[]) { + + final Vector2f[] values = (Vector2f[]) value; + for (final Vector2f vec : values) { + write(data, vec); + } + + } else if(value instanceof SafeArrayList) { + + final SafeArrayList values = (SafeArrayList) value; + for (final Vector2f vec : values.getArray()) { + write(data, vec); + } + + } else if(value instanceof Collection) { + + final Collection values = (Collection) value; + for (final Vector2f vec : values) { + write(data, vec); + } + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void write(final ByteBuffer data, final float[] value) { + for (float val : value) { + data.putFloat(val); + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void write(final ByteBuffer data, final int[] value) { + for (int val : value) { + data.putInt(val); + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void writeVec4(final ByteBuffer data, final Object value) { + + if (value == null) { + data.putLong(0).putLong(0); + } else if (value instanceof Vector4f) { + + final Vector4f vec4 = (Vector4f) value; + data.putFloat(vec4.getX()) + .putFloat(vec4.getY()) + .putFloat(vec4.getZ()) + .putFloat(vec4.getW()); + + } else if(value instanceof Quaternion) { + + final Quaternion vec4 = (Quaternion) value; + data.putFloat(vec4.getX()) + .putFloat(vec4.getY()) + .putFloat(vec4.getZ()) + .putFloat(vec4.getW()); + + } else if(value instanceof ColorRGBA) { + + final ColorRGBA vec4 = (ColorRGBA) value; + data.putFloat(vec4.getRed()) + .putFloat(vec4.getGreen()) + .putFloat(vec4.getBlue()) + .putFloat(vec4.getAlpha()); + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void write(final ByteBuffer data, final Vector3f value) { + + if (value == null) { + data.putLong(0).putInt(0); + } else { + data.putFloat(value.getX()) + .putFloat(value.getY()) + .putFloat(value.getZ()); + } + + if (layout == Layout.std140) { + data.putInt(0); + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param x the x value. + * @param y the y value. + * @param z the z value. + */ + protected void write(final ByteBuffer data, final float x, final float y, final float z) { + + data.putFloat(x) + .putFloat(y) + .putFloat(z); + + if (layout == Layout.std140) { + data.putInt(0); + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param x the x value. + * @param y the y value. + * @param z the z value. + * @param w the w value. + */ + protected void write(final ByteBuffer data, final float x, final float y, final float z, final float w) { + data.putFloat(x) + .putFloat(y) + .putFloat(z) + .putFloat(w); + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void write(final ByteBuffer data, final Vector2f value) { + if (value == null) { + data.putLong(0); + } else { + data.putFloat(value.getX()).putFloat(value.getY()); + } + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void write(final ByteBuffer data, final Matrix3f value) { + write(data, value.get(0, 0), value.get(1, 0), value.get(2, 0)); + write(data, value.get(0, 1), value.get(1, 1), value.get(2, 1)); + write(data, value.get(0, 2), value.get(1, 2), value.get(2, 2)); + } + + /** + * Writes the value to the data buffer. + * + * @param data the data buffer. + * @param value the value. + */ + protected void write(final ByteBuffer data, final Matrix4f value) { + write(data, value.get(0, 0), value.get(1, 0), value.get(2, 0), value.get(3, 0)); + write(data, value.get(0, 1), value.get(1, 1), value.get(2, 1), value.get(3, 1)); + write(data, value.get(0, 2), value.get(1, 2), value.get(2, 2), value.get(3, 2)); + write(data, value.get(0, 3), value.get(1, 3), value.get(2, 3), value.get(3, 3)); + } + + @Override + public void deleteObject(final Object rendererObject) { + + if (!(rendererObject instanceof Renderer)) { + throw new IllegalArgumentException("This bo can't be deleted from " + rendererObject); + } + + ((Renderer) rendererObject).deleteBuffer(this); + } + + @Override + public NativeObject createDestructableClone() { + return new BufferObject(null, getId()); + } + + @Override + protected void deleteNativeBuffers() { + super.deleteNativeBuffers(); + if (previousData != null) { + BufferUtils.destroyDirectBuffer(previousData); + previousData = null; + } + } + + @Override + public long getUniqueId() { + return ((long) OBJTYPE_BO << 32) | ((long) id); + } +} diff --git a/jme3-core/src/main/java/com/jme3/shader/BufferObjectField.java b/jme3-core/src/main/java/com/jme3/shader/BufferObjectField.java new file mode 100644 index 000000000..798b418fc --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/shader/BufferObjectField.java @@ -0,0 +1,77 @@ +package com.jme3.shader; + +import static java.util.Objects.requireNonNull; + +/** + * The class to describe a filed in BO. + * + * @author JavaSaBr + */ +public class BufferObjectField { + + + /** + * The field name. + */ + private final String name; + + /** + * The field type. + */ + private final VarType type; + + /** + * The field value. + */ + private Object value; + + public BufferObjectField(final String name, final VarType type) { + this.name = name; + this.type = type; + } + + /** + * Get the field name. + * + * @return the field name. + */ + public String getName() { + return name; + } + + /** + * Gets the field type. + * + * @return the field type. + */ + public VarType getType() { + return type; + } + + /** + * Gets the field value. + * + * @return the field value. + */ + public Object getValue() { + return value; + } + + /** + * Sets the field value. + * + * @param value the field value. + */ + public void setValue(final Object value) { + this.value = requireNonNull(value, "The field's value can't be null."); + } + + @Override + public String toString() { + return "BufferObjectField{" + + "name='" + name + '\'' + + ", type=" + type + + ", value=" + value + + '}'; + } +} diff --git a/jme3-core/src/main/java/com/jme3/shader/Shader.java b/jme3-core/src/main/java/com/jme3/shader/Shader.java index 72ecbca17..05264f8db 100644 --- a/jme3-core/src/main/java/com/jme3/shader/Shader.java +++ b/jme3-core/src/main/java/com/jme3/shader/Shader.java @@ -51,6 +51,11 @@ public final class Shader extends NativeObject { * Maps uniform name to the uniform variable. */ private final ListMap uniforms; + + /** + * Maps storage block name to the buffer block variables. + */ + private final ListMap bufferBlocks; /** * Uniforms bound to {@link UniformBinding}s. @@ -220,10 +225,11 @@ public final class Shader extends NativeObject { */ public Shader(){ super(); - shaderSourceList = new ArrayList(); - uniforms = new ListMap(); - attribs = new IntMap(); - boundUniforms = new ArrayList(); + shaderSourceList = new ArrayList<>(); + uniforms = new ListMap<>(); + bufferBlocks = new ListMap<>(); + attribs = new IntMap<>(); + boundUniforms = new ArrayList<>(); } /** @@ -240,6 +246,7 @@ public final class Shader extends NativeObject { } uniforms = null; + bufferBlocks = null; boundUniforms = null; attribs = null; } @@ -288,10 +295,40 @@ public final class Shader extends NativeObject { return uniform; } + /** + * Gets or creates a buffer block by the name. + * + * @param name the buffer block's name. + * @return the buffer block. + */ + public ShaderBufferBlock getBufferBlock(final String name) { + + assert name.startsWith("m_"); + + ShaderBufferBlock block = bufferBlocks.get(name); + + if (block == null) { + block = new ShaderBufferBlock(); + block.name = name; + bufferBlocks.put(name, block); + } + + return block; + } + public void removeUniform(String name){ uniforms.remove(name); } + /** + * Removes a buffer block by the name. + * + * @param name the buffer block's name. + */ + public void removeBufferBlock(final String name){ + bufferBlocks.remove(name); + } + public Attribute getAttribute(VertexBuffer.Type attribType){ int ordinal = attribType.ordinal(); Attribute attrib = attribs.get(ordinal); @@ -306,7 +343,16 @@ public final class Shader extends NativeObject { public ListMap getUniformMap(){ return uniforms; } - + + /** + * Get the buffer blocks map. + * + * @return the buffer blocks map. + */ + public ListMap getBufferBlockMap() { + return bufferBlocks; + } + public ArrayList getBoundUniforms() { return boundUniforms; } @@ -320,6 +366,7 @@ public final class Shader extends NativeObject { return getClass().getSimpleName() + "[numSources=" + shaderSourceList.size() + ", numUniforms=" + uniforms.size() + + ", numBufferBlocks=" + bufferBlocks.size() + ", shaderSources=" + getSources() + "]"; } @@ -343,7 +390,7 @@ public final class Shader extends NativeObject { * Resets all uniforms that do not have the "set-by-current-material" flag * to their default value (usually all zeroes or false). * When a uniform is modified, that flag is set, to remove the flag, - * use {@link #clearUniformsSetByCurrent() }. + * use {@link #clearUniformsSetByCurrentFlag() }. */ public void resetUniformsNotSetByCurrent() { int size = uniforms.size(); @@ -366,6 +413,11 @@ public final class Shader extends NativeObject { uniform.reset(); // fixes issue with re-initialization } } + if (bufferBlocks != null) { + for (ShaderBufferBlock shaderBufferBlock : bufferBlocks.values()) { + shaderBufferBlock.reset(); + } + } if (attribs != null) { for (Entry entry : attribs) { entry.getValue().location = ShaderVariable.LOC_UNKNOWN; diff --git a/jme3-core/src/main/java/com/jme3/shader/ShaderBufferBlock.java b/jme3-core/src/main/java/com/jme3/shader/ShaderBufferBlock.java new file mode 100644 index 000000000..27b44d18b --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/shader/ShaderBufferBlock.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2009-2018 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.shader; + +/** + * Implementation of shader's buffer block. + * + * @author JavaSaBr + */ +public class ShaderBufferBlock extends ShaderVariable { + + /** + * Current used buffer object. + */ + protected BufferObject bufferObject; + + /** + * Set the new buffer object. + * + * @param bufferObject the new buffer object. + */ + public void setBufferObject(final BufferObject bufferObject) { + + if (bufferObject == null) { + throw new IllegalArgumentException("for storage block " + name + ": storageData cannot be null"); + } + + this.bufferObject = bufferObject; + + updateNeeded = true; + } + + /** + * Return true if need to update this storage block. + * + * @return true if need to update this storage block. + */ + public boolean isUpdateNeeded(){ + return updateNeeded; + } + + /** + * Clear the flag {@link #isUpdateNeeded()}. + */ + public void clearUpdateNeeded(){ + updateNeeded = false; + } + + /** + * Reset this storage block. + */ + public void reset(){ + updateNeeded = true; + } + + /** + * Get the current storage data. + * + * @return the current storage data. + */ + public BufferObject getBufferObject() { + return bufferObject; + } +} diff --git a/jme3-core/src/main/java/com/jme3/shader/VarType.java b/jme3-core/src/main/java/com/jme3/shader/VarType.java index 7300294d4..2319e7903 100644 --- a/jme3-core/src/main/java/com/jme3/shader/VarType.java +++ b/jme3-core/src/main/java/com/jme3/shader/VarType.java @@ -57,7 +57,8 @@ public enum VarType { Texture3D(false,true,"sampler3D"), TextureArray(false,true,"sampler2DArray|sampler2DArrayShadow"), TextureCubeMap(false,true,"samplerCube"), - Int("int"); + Int("int"), + BufferObject(false, false, "custom"); private boolean usesMultiData = false; private boolean textureType = false; diff --git a/jme3-core/src/main/java/com/jme3/system/NullRenderer.java b/jme3-core/src/main/java/com/jme3/system/NullRenderer.java index 1eb2b1533..3c289ecc3 100644 --- a/jme3-core/src/main/java/com/jme3/system/NullRenderer.java +++ b/jme3-core/src/main/java/com/jme3/system/NullRenderer.java @@ -31,9 +31,6 @@ */ package com.jme3.system; -import java.nio.ByteBuffer; -import java.util.EnumSet; - import com.jme3.light.LightList; import com.jme3.material.RenderState; import com.jme3.math.ColorRGBA; @@ -44,12 +41,16 @@ import com.jme3.renderer.Renderer; import com.jme3.renderer.Statistics; import com.jme3.scene.Mesh; import com.jme3.scene.VertexBuffer; +import com.jme3.shader.BufferObject; import com.jme3.shader.Shader; import com.jme3.shader.Shader.ShaderSource; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image; import com.jme3.texture.Texture; + +import java.nio.ByteBuffer; import java.util.EnumMap; +import java.util.EnumSet; public class NullRenderer implements Renderer { @@ -148,9 +149,17 @@ public class NullRenderer implements Renderer { public void updateBufferData(VertexBuffer vb) { } + @Override + public void updateBufferData(BufferObject bo) { + } public void deleteBuffer(VertexBuffer vb) { } + @Override + public void deleteBuffer(BufferObject bo) { + + } + public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { } diff --git a/jme3-core/src/main/java/com/jme3/util/NativeObject.java b/jme3-core/src/main/java/com/jme3/util/NativeObject.java index 508e6623d..426e4c9e0 100644 --- a/jme3-core/src/main/java/com/jme3/util/NativeObject.java +++ b/jme3-core/src/main/java/com/jme3/util/NativeObject.java @@ -52,7 +52,8 @@ public abstract class NativeObject implements Cloneable { OBJTYPE_SHADERSOURCE = 5, OBJTYPE_AUDIOBUFFER = 6, OBJTYPE_AUDIOSTREAM = 7, - OBJTYPE_FILTER = 8; + OBJTYPE_FILTER = 8, + OBJTYPE_BO = 9; /** * The object manager to which this NativeObject is registered to. 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 e6435e5c5..545bd9c64 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 @@ -4,12 +4,11 @@ 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.*; - import com.jme3.renderer.opengl.GL4; import com.jogamp.opengl.GLContext; +import java.nio.*; + public class JoglGL implements GL, GL2, GL3, GL4 { private static int getLimitBytes(ByteBuffer buffer) { @@ -628,10 +627,36 @@ public class JoglGL implements GL, GL2, GL3, GL4 { 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); } + + @Override + public int glGetUniformBlockIndex(final int program, final String uniformBlockName) { + return GLContext.getCurrentGL().getGL3bc().glGetUniformBlockIndex(program, uniformBlockName); + } + + @Override + public void glBindBufferBase(final int target, final int index, final int buffer) { + GLContext.getCurrentGL().getGL3bc().glBindBufferBase(target, index, buffer); + } + + @Override + public int glGetProgramResourceIndex(final int program, final int programInterface, final String name) { + throw new UnsupportedOperationException(); + //return GLContext.getCurrentGL().getGL4bc().glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(final int program, final int storageBlockIndex, final int storageBlockBinding) { + GLContext.getCurrentGL().getGL4bc().glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + + @Override + public void glUniformBlockBinding(final int program, final int uniformBlockIndex, final int uniformBlockBinding) { + GLContext.getCurrentGL().getGL3bc().glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); + } } 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 5a74e06f0..bca6d0dbb 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 @@ -4,16 +4,12 @@ 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.FloatBuffer; -import java.nio.IntBuffer; -import java.nio.ShortBuffer; - import com.jme3.renderer.opengl.GL4; import com.jme3.util.BufferUtils; import org.lwjgl.opengl.*; +import java.nio.*; + public final class LwjglGL implements GL, GL2, GL3, GL4 { IntBuffer tmpBuff = BufferUtils.createIntBuffer(1); @@ -487,10 +483,35 @@ public final class LwjglGL implements GL, GL2, GL3, GL4 { public void glPatchParameter(int count) { GL40.glPatchParameteri(GL40.GL_PATCH_VERTICES,count); } - + + @Override + public int glGetProgramResourceIndex(final int program, final int programInterface, final String name) { + return GL43.glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(final int program, final int storageBlockIndex, final int storageBlockBinding) { + GL43.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + @Override public void glDeleteVertexArrays(IntBuffer arrays) { checkLimit(arrays); ARBVertexArrayObject.glDeleteVertexArrays(arrays); } + + @Override + public int glGetUniformBlockIndex(final int program, final String uniformBlockName) { + return GL31.glGetUniformBlockIndex(program, uniformBlockName); + } + + @Override + public void glBindBufferBase(final int target, final int index, final int buffer) { + GL30.glBindBufferBase(target, index, buffer); + } + + @Override + public void glUniformBlockBinding(final int program, final int uniformBlockIndex, final int uniformBlockBinding) { + GL31.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); + } } 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 e87bdf20a..0d8c32a8d 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 @@ -616,9 +616,34 @@ public class LwjglGL extends LwjglRender implements GL, GL2, GL3, GL4 { GL40.glPatchParameteri(GL40.GL_PATCH_VERTICES, count); } + @Override + public int glGetProgramResourceIndex(final int program, final int programInterface, final String name) { + return GL43.glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(final int program, final int storageBlockIndex, final int storageBlockBinding) { + GL43.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + @Override public void glDeleteVertexArrays(final IntBuffer arrays) { checkLimit(arrays); ARBVertexArrayObject.glDeleteVertexArrays(arrays); } + + @Override + public int glGetUniformBlockIndex(final int program, final String uniformBlockName) { + return GL31.glGetUniformBlockIndex(program, uniformBlockName); + } + + @Override + public void glBindBufferBase(final int target, final int index, final int buffer) { + GL30.glBindBufferBase(target, index, buffer); + } + + @Override + public void glUniformBlockBinding(final int program, final int uniformBlockIndex, final int uniformBlockBinding) { + GL31.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); + } } From c49a0dc54d2e67b73e31396d43ebff9e3a046640 Mon Sep 17 00:00:00 2001 From: joliver82 <39382046+joliver82@users.noreply.github.com> Date: Thu, 17 May 2018 23:07:23 +0200 Subject: [PATCH 51/54] Update GLImageFormats.java --- .../src/main/java/com/jme3/renderer/opengl/GLImageFormats.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java index 21070227b..fb46b0805 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java @@ -220,7 +220,7 @@ public final class GLImageFormats { // NOTE: OpenGL ES 2.0 does not support DEPTH_COMPONENT as internal format -- fallback to 16-bit depth. if (caps.contains(Caps.OpenGLES20)) { - format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT); } else { format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE); } From c72b73ee8bf14b5d2c23a700fb15e1336ed4b2ce Mon Sep 17 00:00:00 2001 From: TehLeo Date: Fri, 18 May 2018 08:37:59 +0200 Subject: [PATCH 52/54] Added Texture Formats R16F, R32F, RG16F, RG32F. (#839) --- .../jme3/renderer/opengl/GLImageFormats.java | 4 + .../src/main/java/com/jme3/texture/Image.java | 209 ++++++++++++++++-- 2 files changed, 189 insertions(+), 24 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java index fb46b0805..ca8546a67 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java @@ -196,6 +196,10 @@ public final class GLImageFormats { format(formatToGL, Format.Luminance32F, GLExt.GL_LUMINANCE32F_ARB, GL.GL_LUMINANCE, GL.GL_FLOAT); format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, halfFloatFormat); } + format(formatToGL, Format.R16F, GL3.GL_R16F, GL3.GL_RED, halfFloatFormat); + format(formatToGL, Format.R32F, GL3.GL_R32F, GL3.GL_RED, GL.GL_FLOAT); + format(formatToGL, Format.RG16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat); + format(formatToGL, Format.RG32F, GL3.GL_RG32F, GL3.GL_RG, GL.GL_FLOAT); format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, halfFloatFormat); format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT); format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, halfFloatFormat); diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java index 79567b222..fde8d4ecb 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Image.java +++ b/jme3-core/src/main/java/com/jme3/texture/Image.java @@ -258,6 +258,7 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ { * half-precision floating point red, green, and blue. * * Requires {@link Caps#FloatTexture}. + * May be supported for renderbuffers, but the OpenGL specification does not require it. */ RGB16F(48,true), @@ -272,6 +273,7 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ { * single-precision floating point red, green, and blue. * * Requires {@link Caps#FloatTexture}. + * May be supported for renderbuffers, but the OpenGL specification does not require it. */ RGB32F(96,true), @@ -300,31 +302,190 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ { * Requires {@link Caps#TextureCompressionETC1}. */ ETC1(4, false, true, false), - - R8I(8), - R8UI(8), - R16I(16), - R16UI(16), - R32I(32), - R32UI(32), - RG8I(16), - RG8UI(16), - RG16I(32), - RG16UI(32), - RG32I(64), - RG32UI(64), - RGB8I(24), - RGB8UI(24), - RGB16I(48), - RGB16UI(48), - RGB32I(96), - RGB32UI(96), + + /** + * 8 bit signed int red. + * + * Requires {@link Caps#IntegerTexture}. + */ + R8I(8), + /** + * 8 bit unsigned int red. + * + * Requires {@link Caps#IntegerTexture}. + */ + R8UI(8), + /** + * 16 bit signed int red. + * + * Requires {@link Caps#IntegerTexture}. + */ + R16I(16), + /** + * 16 bit unsigned int red. + * + * Requires {@link Caps#IntegerTexture}. + */ + R16UI(16), + /** + * 32 bit signed int red. + * + * Requires {@link Caps#IntegerTexture}. + */ + R32I(32), + /** + * 32 bit unsigned int red. + * + * Requires {@link Caps#IntegerTexture}. + */ + R32UI(32), + + + /** + * 8 bit signed int red and green. + * + * Requires {@link Caps#IntegerTexture}. + */ + RG8I(16), + /** + * 8 bit unsigned int red and green. + * + * Requires {@link Caps#IntegerTexture}. + */ + RG8UI(16), + /** + * 16 bit signed int red and green. + * + * Requires {@link Caps#IntegerTexture}. + */ + RG16I(32), + /** + * 16 bit unsigned int red and green. + * + * Requires {@link Caps#IntegerTexture}. + */ + RG16UI(32), + /** + * 32 bit signed int red and green. + * + * Requires {@link Caps#IntegerTexture}. + */ + RG32I(64), + /** + * 32 bit unsigned int red and green. + * + * Requires {@link Caps#IntegerTexture}. + */ + RG32UI(64), + + /** + * 8 bit signed int red, green and blue. + * + * Requires {@link Caps#IntegerTexture} to be supported for textures. + * May be supported for renderbuffers, but the OpenGL specification does not require it. + */ + RGB8I(24), + /** + * 8 bit unsigned int red, green and blue. + * + * Requires {@link Caps#IntegerTexture} to be supported for textures. + * May be supported for renderbuffers, but the OpenGL specification does not require it. + */ + RGB8UI(24), + /** + * 16 bit signed int red, green and blue. + * + * Requires {@link Caps#IntegerTexture} to be supported for textures. + * May be supported for renderbuffers, but the OpenGL specification does not require it. + */ + RGB16I(48), + /** + * 16 bit unsigned int red, green and blue. + * + * Requires {@link Caps#IntegerTexture} to be supported for textures. + * May be supported for renderbuffers, but the OpenGL specification does not require it. + */ + RGB16UI(48), + /** + * 32 bit signed int red, green and blue. + * + * Requires {@link Caps#IntegerTexture} to be supported for textures. + * May be supported for renderbuffers, but the OpenGL specification does not require it. + */ + RGB32I(96), + /** + * 32 bit unsigned int red, green and blue. + * + * Requires {@link Caps#IntegerTexture} to be supported for textures. + * May be supported for renderbuffers, but the OpenGL specification does not require it. + */ + RGB32UI(96), + + + /** + * 8 bit signed int red, green, blue and alpha. + * + * Requires {@link Caps#IntegerTexture}. + */ RGBA8I(32), - RGBA8UI(32), - RGBA16I(64), - RGBA16UI(64), - RGBA32I(128), - RGBA32UI(128) + /** + * 8 bit unsigned int red, green, blue and alpha. + * + * Requires {@link Caps#IntegerTexture}. + */ + RGBA8UI(32), + /** + * 16 bit signed int red, green, blue and alpha. + * + * Requires {@link Caps#IntegerTexture}. + */ + RGBA16I(64), + /** + * 16 bit unsigned int red, green, blue and alpha. + * + * Requires {@link Caps#IntegerTexture}. + */ + RGBA16UI(64), + /** + * 32 bit signed int red, green, blue and alpha. + * + * Requires {@link Caps#IntegerTexture}. + */ + RGBA32I(128), + /** + * 32 bit unsigned int red, green, blue and alpha. + * + * Requires {@link Caps#IntegerTexture}. + */ + RGBA32UI(128), + + /** + * half-precision floating point red. + * + * Requires {@link Caps#FloatTexture}. + */ + R16F(16,true), + + /** + * single-precision floating point red. + * + * Requires {@link Caps#FloatTexture}. + */ + R32F(32,true), + + /** + * half-precision floating point red and green. + * + * Requires {@link Caps#FloatTexture}. + */ + RG16F(32,true), + + /** + * single-precision floating point red and green. + * + * Requires {@link Caps#FloatTexture}. + */ + RG32F(64,true), ; private int bpp; From 6b2af9917ba92382e85b4d81134f075bb9a18f44 Mon Sep 17 00:00:00 2001 From: Nehon Date: Fri, 18 May 2018 23:54:13 +0200 Subject: [PATCH 53/54] Fixes post shadows compilation issue on android --- .../java/com/jme3/renderer/opengl/GLRenderer.java | 11 ++++++++++- .../main/resources/Common/ShaderLib/Shadows.glsllib | 5 ++++- .../src/main/java/jme3test/light/TestShadowsPerf.java | 1 - 3 files changed, 14 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 790e538f8..372233dd6 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 @@ -1327,6 +1327,7 @@ public final class GLRenderer implements Renderer { + "Only GLSL 1.00 shaders are supported."); } + boolean insertPrecision = false; // Upload shader source. // Merge the defines and source code. stringBuf.setLength(0); @@ -1346,7 +1347,7 @@ public final class GLRenderer implements Renderer { if (source.getType() == ShaderType.Fragment) { // GLES2 requires precision qualifier. - stringBuf.append("precision mediump float;\n"); + insertPrecision = true; } } else { // version 100 does not exist in desktop GLSL. @@ -1365,6 +1366,14 @@ public final class GLRenderer implements Renderer { stringBuf.append(source.getDefines()); stringBuf.append(source.getSource()); + if(insertPrecision){ + // precision token is not a preprocessor dirrective therefore it must be placed after #extension tokens to avoid + // Error P0001: Extension directive must occur before any non-preprocessor tokens + int idx = stringBuf.lastIndexOf("#extension"); + idx = stringBuf.indexOf("\n", idx); + stringBuf.insert(idx + 1, "precision mediump float;\n"); + } + intBuf1.clear(); intBuf1.put(0, stringBuf.length()); gl.glShaderSource(id, new String[]{ stringBuf.toString() }, intBuf1); diff --git a/jme3-core/src/main/resources/Common/ShaderLib/Shadows.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/Shadows.glsllib index baf003f88..c7c6b8851 100644 --- a/jme3-core/src/main/resources/Common/ShaderLib/Shadows.glsllib +++ b/jme3-core/src/main/resources/Common/ShaderLib/Shadows.glsllib @@ -3,7 +3,10 @@ // gather functions are declared to work on shadowmaps #extension GL_ARB_gpu_shader5 : enable #define IVEC2 ivec2 - #ifdef HARDWARE_SHADOWS + #if defined GL_ES + #define SHADOWMAP sampler2D + #define SHADOWCOMPARE(tex,coord) step(coord.z, texture2DProj(tex, coord).r) + #elif defined HARDWARE_SHADOWS #define SHADOWMAP sampler2DShadow #define SHADOWCOMPAREOFFSET(tex,coord,offset) textureProjOffset(tex, coord, offset) #define SHADOWCOMPARE(tex,coord) textureProj(tex, coord) diff --git a/jme3-examples/src/main/java/jme3test/light/TestShadowsPerf.java b/jme3-examples/src/main/java/jme3test/light/TestShadowsPerf.java index f267b1d41..97629117e 100644 --- a/jme3-examples/src/main/java/jme3test/light/TestShadowsPerf.java +++ b/jme3-examples/src/main/java/jme3test/light/TestShadowsPerf.java @@ -69,7 +69,6 @@ public class TestShadowsPerf extends SimpleApplication { @Override public void simpleInitApp() { - Logger.getLogger("com.jme3").setLevel(Level.SEVERE); flyCam.setMoveSpeed(50); flyCam.setEnabled(false); viewPort.setBackgroundColor(ColorRGBA.DarkGray); From d57c362ec3b510c1ba6356f719efa3b1576b95c6 Mon Sep 17 00:00:00 2001 From: Kirill Vainer Date: Mon, 21 May 2018 21:43:28 -0400 Subject: [PATCH 54/54] build javadoc by default --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 168dffd7a..000bf6930 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ jmeVersionTag = SNAPSHOT jmeVersionTagID = 0 # specify if JavaDoc should be built -buildJavaDoc = false +buildJavaDoc = true # specify if SDK and Native libraries get built buildNativeProjects = false

GIwtt;C*Q+vy-ynpev$%d3W* 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�ExF2!} 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ĩ_$GL~&Y&MR- znTNyrq1N4vk-(A0D6pPLdm5vGV~nxrr?KcM8^@vd#==^oN5YNq!12Zeu#QMO8WVw& zjLF7S*c9MYW12A?I33;@#!O=taF#I}Y!1>n#y7yZ#yn#IY`z;88jHXeA-C9AVk`wN zHI{)bN4nft0i0&6G*-h_xp9TD27C>2YmIfrdf<9QU1w}Sy1`fooNa70Hp4c#ajvli zd<$}0jcvwu;C9q>o3R7w4xDK_O3yKN0(T>Ue6Vi*uCF5t~GVlu0tHw3sI`F!2 z1MDW!o5n5RZQ~c?4)Bg~7wn#KA9x?Ro5lkie_%Xx<9!@?0DFkij}YaN@z{;GQR)%w zvGJ?%oACtr#CVGTPmJfVXTTT6OXD@{74VJm){U=`duO~iKEgf#KLS4iKN-K{OPOIN zfMGilGohIXX(BMw{MATo{s!}MqhT^LG0e++VI(nM!o1!1-tacxBeplnCPm4lW-?%M z^LNBfZhkV7na_+A=5ts|H@-!kQkZXyRAy@PTO$qZ0MKQmHf>B}3L~v~8hxAAJZq#g zPar)BO9xDEUNADi&I2zaXJZEQl9ACogz?H~9x*bR-y=N+%LL499yPv#9RhxboQ+?Z z2aGJ{d5l&T^Cu%K&XmPG2g?e~W?nI}!!7|&A!lQD)F20<9tR#ba+*2K{b2hsYjT)h zo4HIMppTi`%!4#9EH^Npncpl3D*!BnoQ(y|!e$Y8e1X1ZQIsrV`oW3|{)0Bm43G#ia_@xv?pZG((Bzu)4tNW(%_=uq7h3GK0<5z}99Pvn|pPSQ}tFq!~5j6;Bs|0Hf?0|YT1UAHx+Gu?pSOs7J+TnoJ1U5#C zD+4Q|*5PIZSOh$c%}!=#=sH6cV0JOPqI_4Bu5Ct|-4Hbr*1_y<_Q09C!-CA7@O1!o zKs}<&Xw)tW7Hsx1d!xp^UxEi!b1+)(4+{WxMXnsoA2`Guiu_RIY#fGk7*vDJ;pPaGAA!&o>Fh}FaAmAXJZ;Uw>wHO2IY>qR>qkiL2yZ+_`b0TUz0oKi&WKKqVCZjb2 z;hhBEB(MSI6qM)>?2q=3HK(Fi#=`oU)6D7Ut!c0ra|Y7Buo&P>4RsWAvuNdZPAIaF$W9?(j}Pt!Bd_QQIM??+~=S3tBw}?H+@c zbV6-MqP`>1+Hjn4ra1>V$NUCtmN^$V7rA-ne4J@M&S>KTqzlk9qoEjKE(9)wY6LtB zVGDrYn2Qi)5n5y8Jd|1ln`SOXU8Vx3noID1s<{+47C6LQhVfWtE(fka?Q9&1+)DJ@ zD%cd@D&T6^DBwhMjky-M7X7;pEwFJCauX1LJ!~;>Jv0+w6M;j}`VGJhILCOzTM1l= z+&Xh3N^eB#ZCs7m8(||+r!nRx;3m{<4C=E9wgPCs3aY*Phk;%}-FwxI>=I)dsRJV- z+t=$B6$Xolj_4Md!1U}n<=Jz}v*(oBFU+&&lxNQ=eQzLr&metoBYm$QeSaT)pCEl7 zC4CQJZr#yR3dG?(0>^bGx zbIP;ll)hh&zSoRr&neHIQ=UDiKJORi*>mdiUU>R`M*7}B`hGy3J*PZ-PJLKbmLHsn2_ydG?(0>^bGxbL#UxVxB#xJbO-g_MH0PwdYj3cn=z+R1X@|>DIGr zNEiDt1JCADp3SH9O{VmXrF6e%^Qr&XhF1UTCQAQR`s<#}r~Y5ti0WV6?B>6fUf)>i zOB)jDns^&d=^IhSZ7lVr4UasVPkA<<`VVZ-=GlBI-Ue9zw>P))Y(C}Le9E)=lxOoP z&*oE}&8Iw@PkA<<@@zi!j~iP3tD7kOSLyYQqV&zAbgpkKrTcZRYxJ1^YCXTS$<&wp z|7!eyW0RVHb<>&uzx4m=W;g$>^!mn9U)qpJ*Tma!O5cboZeyu0ZFuC_e9E)=)PG=u zwtw}y{#UQBzm5N;O`5*s*Eea>`M*d1CC!)o|DO0?D*Gk>mtua&@7a9Hv-y-~^C{2f zQ=ZMI{@ZRoRX7L3i`k4n-)E3Cuv=tU)3At+;n4}qq%OvW5Z(r={qy9t&H(lFho?XE z9`MWuy*E79hi5)`=7Zh?p825nhUfm^nGc@%p!a}hKIpySxj%U3gJ(YIJ>Z!SdT)5{ z51#qpnGbpoc;K48nQq>th~BI|8G zB3nJOeOTXwM)JmyQ4t*@!`cVNM298D1GC97x*qfNfoBeQ=747oc;ejVuw@AO*A<>n>Iz;)^vi~6LGe$&qEZV7L*Y<{yvSCDIyO8#N z4a37iqQj$1BMJU{Rf_1)p;uIxk*0A}7>bAWj0oux)U8Xmo>4~1dLcbSx^xNYTh09k zW`pxSk@Sx!wL`iaNg8$!3+>e;Msz`)LwY7O5;q9zj;>0iNaoLH{AZC2yV_srZHns> z)T%@V98J%7tsjmXjvszFZrYH5hS*Mbu|!q6bPI_#wBAb?SdFn%IGTqUDL&WVw_{kh zuD)eZ>(YqnkCw7TrdRpIrdMSX!sN{KvJaNU!E&kL;BZthj1Q)9J%NEgAxx0S9(`t* z2AXUnHIf<0jTH7MIZQ8ZdXed6nO+r5uRzl)$n>g|1|iZRM1W!S2=8N(t6;YTRPXYZ zgzzUdXma-VCM6DPgRfu79=czIoVa0h&#;iLm}7r#v~0deR<*5F_)T+ac4sJkyQ`6P z>1H6UZxTtT8#T7VAC{wGY^cp$ibLa48kBXZvTr^%v-+eeLYHoiB5;@D)1!i|v2)7P z((O&@Xp?4Ekv)NQVr;O@U5Z2FQkuhiLaYqm<)(GH`;aqRh&3kGmm0=|+T5i$G%lst zpQW{R_Sf80>CJEgcPZW{yoVK+Sq0AKrw7dn+T5i$ zG%lqr!QtAM0$rd^MZD$asN+ zyA*$vvAxxLWFCH|)?_}kMW_`Z2J*Ts#@O7YI5aM$sd~GU^^`O;L zN5zF3Z0=GV8kf@a9NooAwxbE3>wT4jyANS{RX(}`sS?o#|s{i@c6On$V#*;ESLTG8^F zTbQo)baR*D(72Q)W35`&a8<}SsdaVgD~)#a?U3qtty8jA_srTE;I1uR#dL?UcW3{9Wt zXVq<9+gXO{P|2Gmtf~obIgfwe#O5x=p>Zh<%DPn9&htxKNj7+i<{u*n+@*N3Grm^1 zHPG?hp)Mqr_*#kQuLyX(u9?kUibLa4np-zYTBl2`j_ndSf`TR%v(C-PBVDX^k zE1SC%hsLEeDC<&X_b#hq4cReFB)Xb{z+H+@+E~siS&ECKMbpyzJQb{v?!Kac?*|U< zQXCqW(#W=Cb$L8etp2VDHTt%^<)6QU$Z|E0&0UH^<5HUHzLl);nZ}F$FLM#NOYwA- zT3G3Me{rH*Z7zy7wIVZRadwIr#1pP5kBhTg1(IEJc3n4IW^`q@ zF3y|d95a82+_5ab^*mua0ZxiTLrR0Pq{^o6-9zrpklk94<~t`i#qrtW<>*)Q5{Pe+*vedKGR;G@R`0k z`pTHZnXLsQrtwctN6FH2zp_poo@jGY92!!Z#MQdX8+kHVFKce+;H3D)m-%II*~WbP zkt!7MDxD1bScQMRtuD2kQCz-0UYmR07-(}+92!y@lqFR*_v?gm|EJD8EPHDLC&iD| zEF&`~-p&V&Z$kSkl$IqMP2}n4)UY`z4h<m|Ra8kU|_*Qa%69-iv+KX0ERk@~KX*ygqh6?6tC99mNOqUwZ zvNG68j3D*^6!fWwT$6DL)lV}R{oP-Fs=A#944GzgQXCpm8k8kfHtp+Hl6_oG`vwdpa8i6x zkD+o$hCQ_T_ee^3bflcJd?WeK>0xtH92!!Zd3T!2LhJTWigyG)Wez{)tYPu0RKBPm?o4UeN6{m26Im zLqke4e`%CVmy%hHSOBey86o>tHmvYI7{Sde>_ISdk1TGcdFwUY12Xj(^GtI=k z;z>p6Q+?=Ezh+{X+0Ah+OE|52aE^mJ6^Di^4a!PYcF5po{NC_HG;L#P0vC$kJi3yP zEtrNMsMSqQSX$M&W<@Sao}-G*9g0IElxBU?UgUpzs#v^#Wo+A{{uG>XxcI$qZV4_G zhej$*p|XuBp}8R7>$xciTq<6ey`oE)<7GD$VrLJ!$Rpd(P%%=5cVTcyjZY z^I1qyO1{;fzM9%hUQT0Dx4n7!np(AG!vwVhUjLGrz)5jvNNG^kp~`MOzgYNh?<-zD z??m8S@#o(+<}*h2p|TA!ioDY3T#7n{&mX5lS=fNC5S07{#k!%0){qmY`{~ zN>H=!eK@#O92%)KFV-yMC0djf2TDW|xKO;>grf3ZiF1zVR{pdtXn@?D;BIV>n}x-1 zJ*&&QNwUj?J;yr0NpWaMX;79_+2D7(#E^E?)R-G8Ppnxls$WVW ze1mF<4~_fE-4$-}O)YPWl5eZnTqq6=R~nS%s%*z6xoOYp@^Vb!J_1}S-nUR+ng3lY zk$3BF&N7MpWs+&Lo$2rSNpMme8d930KDEX2ELEh}>91&DKqZ;Kp9U`(?I`q149(rzP=GrXhbBO2P?l8L?LE@d>8v?uTB0Nb zE)`GP$&``r&+~NEEt%Y?CzEYYMn1Rd3vf~#8d91I2Z~VMoZs@zhjPgj2Vxz+R$R@u zF79q~r{d5^r75^6gjz3p$}c7GjRlvAPwu)zT$);*2X~32jPER7arp7r{f+w5t+hS* z$N`OL5*aoZibErm24%S_yE$hW+HoM-v8iH70+))v&hbf%JJO6AoDLF~au$=5DrBJG z23I&ZDGm)+nqBjA(=Xfo=+4$H;_|Q+0SWrxYv}8jHjh;t8mTmW+jgh5->l#vH-B@0 zOT{PtmOu`v(pK65^)FCA?+4wR{5>?c{DbJ-Fp$8x;?Ovi24zW=En4;s zZys4%ZYiCBz=h&v65w3V96SlWbw<7HCMQkXBnmGd-~=bdp&_LSzFEz2alu0VbNVQ{ z@{PA_IQs?fal5$9NpWbLO5+^&fj@6vRCH|Dg2091t)}_Ou}{j%X15+WN{2pm776Jk z58W8S1FjwvgL(`VOR9wuI9D7Rhti-dsj}8k2kQ{HRJ?OxE(13|5F>wl%g?naAhNtJBNKQRrTb5Ziz+R{_=E-BZO#>khAR!q zk}5m!`&0b#-iKnqoazKF6#qS;pGZvYL7z-AX5%KYEO7-o zZaxlJx+}Ttwzn~z@@eTb2VdlMXLhu?Lvd(gl?G)=m7QPeD+=#DQB)po;hV!(Qt|H( z(E>8d4hHJV~f#^E#B_(+G~Wl(XTqfmCAJ6$iLO zacHE{OuE;A)-@l^XEkR8E)~aW-|M~oD+2>R-4BV52*pqL@ynN>Zuk{S0@HYN=}2~( zcCrn5Qcpj4=7Hxr@Z1MH_W{p+;D6420H6Pf!qrdvK7alv`s2Ck6zaL^hT+|MMkg}O z#f%vY&r{gXCEytWlO<+e$k{vz^JYm|GB@{zDj{;VKRHXmQsOZOo2P&(8FGnG&VGC$ z4NBYo)KH~DE)h$M=MmDg3@jtd1fCwxkbH%l&9ktqEE~)2=2@Z2h@9=u!E&;%SuQuv z2~~FFY=36vgXc)Ta&sT3av_%qk6Pqq`B;8dfE5JK#|p8+$l1II^JPVupPTzaRRB5L zUyK!JC0I!}FAkL-a<;z^E5%Bqw9QLFRT8;^tPCs5$}xXdo>c%Z#{!rGIh%7v%wiSY zoS-U?ob3-}L97yt@zs-m>bt3Xv5xeBZrtIle$nyeP94PJxQVRezS zc|BI2HDC?hygpR5khA@bSYy_NHFfjGP&Gu(_Sa#}SaX!Nc{8Y*B3GNWU@ciI7R*|+ zHsGyTTNZ+x&D*h1)}DpAc_>t^k+c0BSVtDlBHX+qRAI>3{x|Mi?*vr@a&1@_ z)|ExFZmc`&0UpVEvMA(i9?g2O-mH(C_kyZBa<;!Oi(&m(e>abTstn^- z_K#qb*kqKp`6Q?&A~&2(VN=;OHl592Gr?xL`E2Couy5RaE}I87#m#53xokdL;N}b2 zBCt7bK94O#$;EEIgv|z<@8*lx61J2rW6RkJwvw#^TkYm+kXy^vx%qmw0c@$8uVU-j zMz+b#H?u8ZYu$VU+l-Q1-FzEc1Gdr4x3Fz&JKMo_vR!O9+XJ@O&G#V}%bad5m;~GI z=6jf6F1Fvz53p~+V%=P_11NdW&A(&&z+7(rE&GlgVu#rgc9eb3j)DE)=Espc!A`pQ zkL(oKAvZt9eq^WF88<)6&Vik9^Hc0BN}hM~3+y=9X*WN|F0hO2Cw7Vb%r3JlU{~Gz z8gkd!4L85ZZh>8N^DFEoyUl)a^E>P=*mXC*#qOZwJvYD4u7TZl^SkUmd%zyDN9-~C zmHh_x1pF!ZWA=2_QZ3+9CKOJDt2K7Ky+zS8_q{$Kf)cRMjp7|TAoEN`*m z5zjn(mpi{;dnx|ilPCP~_U(V0kFSp(Z{4V}{Hfy!-}`BsyL|lo``FU_uJM%5%lSF} zsx_r}%+n`)>z=>Q$JfsC)gU-6o7e60gf8tm!USehdoe`hQ z`{8^8aK7C$|2`jIe`!*79?_hWSEkeM^Tp54EM1b$&iKaNcXt2OELoB|ee-Aj*NcV7 zr_S2AsQ+B8{_*qi z^=7h?^x^)W=Zl~3e&rUgnkHr3^;&+?Eq=S)e>@*Q-h1CW+`OMWE`I#{fA)N#sP9Rw zzQ>WD^Kazi>;KvL@kn37Z1KM0{~Ir-PVmGVlT2AMaAmg7=JAjHU0&Cza-KMU_TT^H z{z;Bk0mfNwaAmgN=IXfaSI5+GmB!a9ze?kmi=T(CVAkaSC6*Nx|I>1VE3^GJSI2d~ zI;M{2{g~h z%P-KZxcu*5pjj60$ZUvrWH$Zd1)9z90?km2j{60gjc~9r4mPp>0>#4HFPj@?qK~GT z_?`O|nhC%33eA?LS1Z#i*z{^`dbKgV+L~S=rdK=l3e6UV)hBNr)0%YWNdj8DsC7aF zPilzYe}7`~?tPx%L!#-a&|6B6IF^b7y`}U&afj<-^5oPz<1#8NtGmvuko@?Oox~b)SNlYR z9rbH(p#GhTyUJ#4#95=6w%<~I=q;s3981N4-ctJ2t0P@GR^Q?uj$Gk+^7e5RoIZ~8 zetraQDGrULG)Zz+GB!6jWgou%p3%<&}W<#%Noa)|%%Z~(z?X?{!j6F+V3 zI(9xI)o2^a!7arn7HH&J*Qy$SJg}1k+|u~p`Z4?2Qg0ut!;+E0=DQQ-v6c)UBuX5q zEtaIsW92T|U5Kl-#pp~vR-MNKY(H0iU2k(02YRmbwm-q*VH`LO!!3RwW3o@ch|?!Hm{YY+*|=a|2zNr8_h(LsN9x!*KgeV++n$`BeNF^ z`1#-Yr*v*8_8-n=ZO=8?t-rV@mlaiOmVlrCoxj(*yW+!mAFJ8nisHnTU&ZqtKGu+3 zZEbyA9KC<^IM8#YM;xx==>2o|+vlQvKpsns>TcILt}S}~q2Wris?Sq#=j(jd*{~41 zPvZJY?;mKm($uN^Ks+y>&-$%b;1~NJ8m=^@+x#L1b@H@U3) zJN7vB`2as@{lvay1G6p-PafnmwyEtr^~8qQ%ksO^*XTDLvwlilg_N zt3`ELYGgJma*V7v%a_ z==~23K3hI(tm5UR-->M3(8phMeQxOE3k@kvwpoGle&saQk=!ocdS4~^{fAUm{u0}` z-fwzLy&s?_rAHi6arF3wrdE@QU!<{8r8vg*w!lxCKd$~UTk6T=&C^)h6CUMyU%^kB zKdyg{oo^r?r%G+5O&81cv4Ed6e_a2IpKHk4Y0fu58eaY)6{`>k-MK>78Y-|4GzsY!2(UVmsvX%7BM zvOy8U`Y|LU>3ya54>Y7SzKP1o{BI3w-nxvWkA>d<(2&x63MwJ@h8fnPW2s4>8~XS{ zLrU{DeP&rc@g1sgGl0_jWR_{N-J&_m%aGo0dQ80^peLnA98z)g_=~gVkUu>6g?{=q zko2~|Pntij{_Xto$RbZ~(ivwU>3s!1Y5utWnG}#mCh)pW#fAisJ{Itk=8x*T>TR~+$>yMV`=cu_|>tM zk~*hR=0sz~+?~6q@aQ?TvhW-+epM{h|9OJVx#G~^wZfk@D9csZdShFPGR+Rq(q!X^ zuJ;gC+U})ao(;D-R~#CyG@E-zi+-68Q17fW2%IY(&~dz28oY&mIyHvCx#DRu*A#sE z4vNk{iom(zNz--~GtzFPiq$3&I9L3qpqk=x`|Z@X(o6#9ijS_HN9_51FLih^k-)j) zudIj8Zcm(4?fpb*8WSk|pX{VIDTdkSiaW1fR<8r}TbwSU_OC!#m{uQ=bXp((7wS73EZLh%0iEwH>38@*m?od4V zQ;?Ydb^&GVvW#}MtuF31TtRgm%jwmiQ1MlxdGx?*f$itY4?S0UXt>f`FI-;uw_ipn zCM+j#uDJajWc1aQE+p_ zD-I1_7WY}Bw_VpCUUbCyfX}7;X(tf4LvcNh-Va}Dd!Dw9#Qr9W@DkiX^rD-W2x`2R zf*N1zV)V7C^lj)+f}d-C zuKaeNJRZK5o|F!E_xZC&3-NuolbaWgoI+j8^rPfFSlkF&Kx^JlusK(J$Gh@kQHAOB zYUgAE=ZgQUdb%}qzqjs}}4&w!K8eD8hFhwa&W@C(__L8|X%V>D+j395s$uWpjt(&^VMv*XN4L>ty36QL0^=C}GwM zvF(P=q*?vlJV0@197?0d**oHv^U3Jxbamne0(U5`$I;{4?ag^~8iAZCJj^-6v&NO`>WO#?hu($?%faDby!+;$Ozs)~oTtxW|nz^mxJRXT3gN`aIF+ zhCYsV%n7eX(aqZ{zu2GWZEyI9@E!g&~T-xSb3yao;jA9eK(c#^`5@QgN7^3wugg+T{?yly&~T+m zoumjcW0{yx?FD~PXCmJ@YO4qtUy<^hTgAW6= z6|Pf|UWTrqNeMDouO8mvmoGG?>X%Ym8($pYYdb6_pYL+uJ6(Fmvquf+MV%~`qx#+o z;RzN}$2}RX8CPP(58i>)7d+g1s~G&#SaP&CtTvB|%iLFT)4ays7WYms-_#GIjLS}w zPu)&3xZxB2oq3Bw3s;gGB8t+{>uP7_sPR4l&?YykvApR_S{($kFKAEONq2U)iilQc*o^5{a=8YAi5DLX8o| zPO7oP7)fo6q&7xE8zZTWk<`XWYGWj|F_PLCNo|azHbzn#BdLv%)W%3^Vlyz_7$b?qoce^l3n~s?Yj0DaWzJvyRuThGKC#8_vW$3h}0PE zzF$&i+@DHJsF^`FJ(W{lDt9wr|Azds@a18m`Tm^ZWKps?S3Gr+uPNGVOMtPWxI|e} zWkV|k(&DSh9XsmhqJ!V;;{C?W4;b6GBV9eVfLm#Iil&ZQG_y)68b4vBaGXzQ`-SpD zFO(i}q>2N*RQl`l@AJC%&T&Vzf}IBOt^J%`cVzfGkGM(=DUTStsCrHI)W0pG&>I?nCfP%`cTd#i2^Hd;KLk zdbcaB&$5u@`IF>q6i)C<%`cTd``YT%vvUF~N9i&&EqECn9%@(>@U;(qsrjYy2PSGr z)r+RLo;Nwp*SuRslbU9>vebFX;g_0UD!+GRJsP(uyY;5~(SXwV@qHk#k9F6-odbTU z`K9t7Ib~6==2@)8jkbvn9T!kr@W%y&fM05Usr+Tj<)z;5lUW6xW|zk3ag;e}LaWBv z3KD*)`K9ug`1&CaZ+(aQo~~2S!Kb^zRvU^MTh31#a-*fDA9=bD@E@bYGdF4lb z2NzS@DZ?cELi0=I|CGF&W5DUr^r2paJn{Z6Z|!VC3Fh>cMRt{P4s1D!Mqlk``-So! z>=P(XO~uQ~r?-?7+WYZ(>n`$1Et<@f8DkB@*5Fr`Y z@?JB7Wc>`!of)QvI0h`JBr_*E%8!Mu<6$dX$*N7qI%+z`J7&bz`OAGzzaQ$?lzy%0 z*U`UmZN^<+`n^}Ln_eIN+SjjNz5e?BT)&6w_Yl2Z`n9j$PxL<2KRfh$l-{5EXNo=+ zdjIS9H2t$lA8UPl^?Rc}2lUT4@VI-aJ|FZsrhis~$K8{miJNcwXQ)0`_4x@++&x_X zEY`0Def~oe_gSD{Bl>4Lc-&_PG;!C9ehunt1Ms-dAZX&QM}5tpU(5RS3r*Z-mcI7T z?-ly}0h+kaKK&k|uVKLBJ}aS#yPxRyD19vi9`~6GP2BxTUnA-FI{p3zP26X+zSh$3 zrTYC7nz+w;{T{2Y*}&u02GGRaZ}oe)zV-u;TVp^IcR$zHg!*TJe*cFiZcU=EE%nbD z{qqHyxV4S`8KkdK!Q<9K(8PTn>7Q}>S{FQSO$ANd=bgR=);}xt&qrwD)?oTtTK_E8 zKYyW#Tg&O6(fXPkJZ|j=P2A_Tz6Q|O_TX`ANND2L1Nxd@UrXrg2WaBfoci|&eLbVE zRrK|(zW&j_cj)Wecx#)uwUGW@Mql&j>ty{qj=rYS*VX!(YHRF1UgAz0tI(arVsD-I zywt_!)|Hs=#oms;@QZ`nTCvA^iS6sB^DQgeSc`8Q7t^A%b7Oyq)%1iA%YX01TR20l zxwHC;gjshvGA$0aYW(`O@cFioqfTfWD<;5K)Zf2`Z;8ORvZEh5s}JAjnBv>Ux>>;x zqnf4_&rUb7c6>-KFV7$6thB$mb$D=InSc1Q3d77$>wSgy;=TEeEV~4D{&z|8TC_mg$-|8S#y347MW807Q5+q zu;s%Fh~xK51Qfd7#>z9Ng$V0bT@=sT#tLj3R^I-8J^Ev3>(`)H{B@#O(dM@hYfINt z4rsX2z|WQ6*59udY{hJ9;at`1idcOu)Y_7zKL_WE7cTx<6gt?!>Qw4J2j_}ot2dE& zM1(cud>68$pD2?n+#2HB-`)a_Yksc$y-Qpaw<98~ZGp83oGWhEVZ!;2*0O}dDdff@ z!E=XO^?uK8bFO&FvN;`%FSW76*bvbsW{Pv^kL|2uqNb?ZqOYS|vG!KBQ@*0g>|M@T zLG7))_a-~{yesZ_w6eW*w9lH@_MGwbN!nOnZMfy^YCdva@d>sXk9z9Ny<|h|j9M+N z3WwW@K<_h-nV~JMjHAklh2F;e(IgYIc{2N=jxlStsc21mmj$$QH5SPnp-~g zk2vi)u=9^OaIs1k?%3Pj>eZ`{c>1!mV|3?s*24TLg+4d*IcCqL27k<@no|$*5xd%2 zS9UD_%be8bsy>JHxioX2m&kOswe`60VX^*50cXDTA(r>eBJR22_s87O=a@c!ay9>2 z1f*|jHENnx>hn#Xhx#1W=YT#&sUMyb;W?wNDswZ_k=A!bo~k`8%5s&%&y`;vM}7W4 z&y`-E8@;CP74Hf~TPDO<1{4ObdpLSNYdI3&b-??BrYtQQQ zPWZX<>(}y$27zMln|{{9m>3Su6+atxN|fFnX&sQ^_O%>$UBk~cf7~^`y~2D^GrFG@ zR?nc--Ij;~`}$fH8lT|cTybc)(tJ8qRs7y+fVIACJ_6^8+jV}Gf1s6kQF%(3V6n)t zzn2vh(36|V`->S%ds=C}J>l3jcYzqxq>J^F&sYciT=}8rN)HWJn$DFri;{IaSkb|K z9pGH?AKT{?_O{bacBXQIySJdW`IIY(#EMs4te6Bp>;uQo3O;i$?_8qHraqRLA|nUq ziqp{r&XkLyEdQkS9N-Sc?Y3lj*T)J7PwXDA=Eb{P{UgT3>i5<7_tM1;c8H8rDmUBt^eH&`~x$;BLmEJzDieq!7_wnB^h7{i8 z+&iMGwEaGjC!8?}YPosW8~4S543XK;3wsWo|GPONX3Dkl>W_! zXt^?3aZzxy#m9^oDYy4dCfg=%Xme5=8d933E&55HQl>a?#al#)aQS^!mvdmNpM~$q zj&jfVC*s(?o1%KkQPR8qC-JF&YTHlB4?QV8;*g31Jt_U!L4NYXgmunh9Sh0b_ln8r zU+;+gZ;DFzN%NEP>wWl5m1W}8rpF>jy%KWZ#w_w<_&qU4X8cPW_(}Pp=Sr{lInEmw zzgELavQx(=qJHs1e`zE9r1|68XpglTUz@vT{U!<~s^}hfohyw#c6Hh&mUR~7mt}Hh zrQ_4ah|ogCWzuSXf0>i;bLEGglwKcGeXb@Q-cJ5d=z~~rF~MKP1%A@}as98)tA+_H z$+BHC$@DF*a&S^ypJVwKwUd>WdC3=93V(6_KtoDn_es6_{&LyOvVXb0^l?vHG9A9J z9dW)Ll~hJ8SSSYFIOk0K!pFWw;;t$9x$;AgpFe-r>+?jvru4bGnpx6y@}e_S$X)?X zitF>!uIHfAcb&A>_b=D4ey<3;KU|!mC1U*b!W7fzgJ>T;Mci|yA^n<#pDRD~TxSpKzI*GXH|K15sitE?Th6C6RIBiFO=WVXl8$8UWc{N41V7jOr2Nm>2$`tPI9b!kKr_$hmJ!Ar`Qs2y@RQ~z z<^Q;Jq1+ZUO8V~{#KEyC>1ST8&LElo=v)As91iK3ONe7 z+83Qu)0Waz6A#h6DptrCIqS{$q1Qf?Qtv^&$QU{M8>TBe>3pEmd^;CCUH?kCb=)g@ z??x^ho4tf&_o^+=xqlEm>$E1*_M6HJP9!T?4m@YR#C=wW{lMy`>mSn7 z`>gS;sWDw$r-p1F;-q5ad`!Lp+*xoxpH|QRN<%u>Cxf zwQf~8pr3YuxgfVdbaqKEF zx?e^^r*x!@f%CdngUNe;Q~7L9Pf zeBgdl%V9Lcp{G2}*c+#x&Y%OeBjlgq-<;Zh!+r({PK63wE&ByH37Ra-!I%C}*4`?$SBzqqE0W)vyrwLz&%B8<8 zFsF0_weagA_xm1(EJvO*AGy!!Ag{xGY8F5(VrchM?dR(%57$~q=V!FYVu*A+-HxWijgyOkUOdU z(y9K`P+rHq99wwD+97eUhbHH7%Km+xm!S2709 z%mMD~Na#p2_qLStCwE20$XQ(1Z1X|#`H7=^$oV2+44lQ9I>XM=8U79AXVVnyoG6nX zacd?|SvXq7$eB6Foz5qo(dL)x$;n5vfH88OyGo`{`!tmOd>@j1m+n((h^d@0+EB&F znK}4R&f=GIY4fXAa;H<)lreIC*w>NPyV_D7v$i2+jGT3|zmc6sx6>%Y&g^r z)i~OnB+6CS1Sn6{vvDOip?XFhVD z)j?i|`N)0!{W)~godmi2hcc+=wT*86HB&ZYuca6xXXYSxdRCfDt&hdYiThWOdtpmy z>yh*1PwaYamZOj(&s$|br(=`G%k@592fq(LsLr)0S+9`o6a=uUPe&?proSK5_QDvas_#8tOAf&bYIa%r`2h zpFhu*2OT~_MxW7@H|5QeJ1jh?WI6Jj`N(}%2YDUlyK#RJMq|p!sLMNPwrRTje2+e5ISM)Q+$FP^)^&`Ne@(82 zi5}-&1Z; zF>+=Oa)+&peBDJ?YRVlZWyxj%5jgXnft-l{T{aLa=?xL|bbult*fD-9R=OnI z?{(fsH~tQhlbTtna^yMlk^9Es_wiA!fwIff-!Ql9K6E)1Dxb-+LK#-me%7h-=WY6U zm%VP9S2;}H-(O#qBhOQv*)?z%!{jeV^zo)Vhn-Ug%e^n^(}>ZzG(38;>^`L)eOvbg zeR4KYKHH%I-QHpkb^8`52kxt(%8}>HNA9N-&%vX8Lu8W?`ZTcB9xOc?Bv{rwSXN-@K-CG7Jjrz}^ufwOx2J>e_k0GPzR`UqC zxAk;jIr4mc=qQ925%PPj86ega*A=lo(;@A-TNIdq6 zeIy=x#W5_lxp?f=I3}x)v0`6{V_Q7-ihU}M4e{8kaZFYpW5qEe_O*EI700=F>=lo( z8e?9)kBG-!abFORz2bf?9(%=OthkSV52!7_d6Fa7{~3e(cRZ#;x8%r!YZu~<3)SSF za*pgVXtRplI5P*i!`5}<>qbq^rLR`y$W=!sp#Q$T^w8EEdFzRtDn`!CLGEmBQCmKC zI7gPeFTl-Pi|OMHIr8|_s>m2QGslfPT|ZTqmmJNJ*X+_KCtlB`mrHVFx_pep7&$Y? zjXO1>7t)NmIr5Hz9ppiOUD<3}j(qxc8DTjJxf{>B+}%qv?&Zky>sr!hAMVhAHLOkQ zSyPsykR#7~x41$t=BLR&QzMZvat^9?pSB#4DG%IEkTG&L)7ekMA7;yAJ9DYkOA{#>ly@pSC zW8^$Mt%}@XB&)e{Ju-IV?D3+2-oBSDXWVb9>WhEbN3V?T(9L7B<<=!us=ibAk=S>v z4)Qw8NACZewtzaY=T$yE zmp#I><-6ash&YDD@&9}7=+;|=4yK!IKf6HH($UjGA%R{^; zptx_c+>PhYKF*;vj%CZ4*^^cK&cED`n8U8K{NsEYxSHOYmo2wgn~35*F7C7TXBW}1 z)!A~B-PQiuf5rWs)j?i|`N;j8f&zMj^B}SK@dRk6q#z`*+7NalDDgG;!>y?)~%e z?|c8~^Iv>2`aH9p2i_lKNL|iugAj*S=#<)o7L%i3z2Ph@ZW=)!=$uzfY;AxyyAPmE z^@b@~PI%6I%6(Rc@;c0?+&^dXMp@7xoVw_qBA?v0C{n)lrwjU3p)8k#yol#tZd=hV z#U5y%zgBt^;7C8zbHM$X7BG2P1Zk1yfrZN(;GXe5gS-y& zf&1$|6+@%(PvGpa9+>)I2Q&-13{5BW$C5RUSah-tIjL)p>FoBax%RrGt)aClr#xpq z;XbQFcpc_b?pGetmv(xR2ZP@~C;nH%>GV!fY}+@h7&$WsxijL1F?%JvpP2Pm(5^Mx z(EbY_kaMZERg9dOga69WSodH0C1d-d+nNvLY5D?i8qo|_c5$Px%c>HVQz553fA!7+ zdbvonN}3I2Ow?SnHqF1%3r`p{cXSI`AYMZ)HpwBj-JV_sDYX86?YUAY}{!|I1wUF7&Ue zU1?BadY0wofr8K52m;P1W zytHC_loMMdRQPpz8Vi`&l};0~=eisaQ8h<9lj5!*^^ALameoQHOSCC(u@``qn++y2ZU+&{i}9-C9r!S>X%n1Vq38sgk0S|yI;D~tvUsYp87Ua z>{L0?c%36zx3onsSXwE zi0vlUVLou5)uFtO*ly0rqiOoPT;-WFl!(to+}nI=yIUp(Rk9Nnl07c-J67LZc5JwBNgk2?IzY?K5(Dap}daRZjl`#sA=g*qKwX0iqA#d ze&!%|9yxTNuL{ROVHaBz+e&O7<{)jG@pID*Y32+G4?HMV!azGJwwqW}YzO93?z1|S*AeT}cZ;M) zxh=4fo`m9aVYyPs)!Sb$RH9$*%hHW>I~3cBnMr!=9ri%%^}taY*rYv<>Fo}k z{aaJ9-Nc$=J20PpYwnNF>QG)stk1TCY`bNH?}oXe_*_^HLayHa$$RbT!5cngy0txu zZN+jFa&`OsHrJ(tR|e4mI<`u&FIbL3u5SPQgI?hGr8%8@!}+iAz;Ytw>V9AT*oV#; zQ-k)mo(9|QejowG^`)*WgH>0A(SeOm!OO39><*D5ku!6EJ8WI#>n`a1R_Z@^1S*WL zk{VkEkcHnIU}cL~${08^N68&>T_0vTc%u1-Cdim@4tJ?cSIsk_*H)NQ#>n~H*IcOE z;~-h&9ZVS$&W|tDAfYppXy0E2u8e`RW7K8npT9;KeQG2!CY(!p_GbHCO{{HoMPkhM z%|Gl}e2%2;49CeK&QjS>4|@K5HC)i3fM7;<+TXwgGwNHCfEfmK$L@YO!l62_9C^-s z;J>!n1$Xc)U|&=%mV zdJWP)ur3^MPO5kv^!b?|)<`qPyADsuQsa}-W&NQl2F^k8Tge2AL&}yHqtI!@E|UD~ zFxjIBLY9M&1J70UL#K^Zy!?`a)(yr}t@?GyEu(pmAh)Dx`nzECGizjwoSB2%*<5WH zd!IoM=IP#r^R`-8$*dnP^c@e~?&nIL#h)ePUP;JU$(cEnJ8WI{p2MGY>kTu(z{%Eh z-j1KZm~!s8z7M-&!ekP1UyH`+ULxhY-$|<1BlgcrNVeHLl2|oZl>^V2kKAVtaK~wQ z59(ogo{X${i$r|dPvGfma&l`A%5oHP;JKh1D97(s0QC3_<#yR^? zFWT}_InjE!2{agmT+JarCed@b3Yg(+1R9J&uI7=QOG*7TUSxy$FjO|mCE3N}VY$mF z9Cam^G!62Hw1=Zr44jz*++phiUw7Ez+oX|BXKBW`5y%)gH?;9X?0=J# z5o6%Y9N>;>8x~KDqwQ`ECn0{?B+za=d(DugV&H7{pf-Kcp%*>xky^wUIg4#;Z&i=J ze_EM3NE~RI^59HJ9A(XMiS>-=l=YqxU z+f?{3ex%@jZSx{k&RIO~y0f(zy9>ljXj7*nos#7NP4XMkm8qUoJa1t+;W_i!-68(? z;`x|(K2!MS8Ckz?D1GkHQYoG<{dJD?<{5CO@-lOJE_@ytQOgy@^D*%pj5)}iw+@bE zMR9%ff9I__?^K`fiRXCC0q(qq*KpoD6f;BJB=P)HJcncs;m#UeLwe%E3h=1vO+|Lz zug9*=Rf1c0FDi0dyZhv6Kt~(Vu!H1CYs>7hcfe}>X@4IG`F1@pqOCC<3BHz*d zpxkY}o$Nm~hKgLOT}9gKYeFO1`B9NSEj57)dh8qN8){IIdu`Hze%+F2*W9^Ek%w6{ zlMWmWrd8X10Fk%qOpqRpG^ZmEc4qIv{1>0L)rLxkFOaTXN216RGA&?CcpcL3TO9l5 z;D2$a%*KG3GbOzNi74{5)L&ADpCLR-O+b+k-ERi*e$}9)T|A2XI&L2HuWApTccW3{ zpAI|W=csV_*diQR`?D*e|MUgxBNX~e&7#Bl1%y6QGwT-+`bf>JUqI+1HM4$^gg#O; z>ldZaM`~vMLWDk2GwT;3^pTobzbJ)1QZwrprO-!eX8l5hK2r1ep*NHf4wmFPCZNc+ zl?D|BbqbfZW~89V@mg~f5B+=~=6O7d{Q5$n5?;-MPV*+9$di^%A>W&BhrKsOqR6iP zo5=dob8w}8Ailf2o#e%jBH7)-X(yd|* z8i`slfG(V-hh!=xr9VAU&6TG73hZ-yixteV)Oo;i_-e?wE9W@w)T+I%dI>fbM5{5O+l{6THT+J;* zO_eKd4aH@PpAij~gj~(*Lx(DtSz6+Yqn=cQB_UVy(P%BAQ&W!ot3_`BvbM{Th zHKTg)LMNVz+~V*xDIt~JZMI_q75U0ZGZL+vtTbJkNJXxt*F_->9iq%!nnFdM)ajeD zd}(zuVod@SIWTo@QNwCJ48q?6!80NUaPjVPI-?N#t7F24TvYpX^rz(v>2=dl!JMD(lnH zO${WGqcelpH!O8%x<{5Ia@nvD)ZP7@{B-!D)L`}=qB>V|Lfa6!IQ|Ljb8iG9=X;0X z1&bH(e%u#H>G7Gfu>72W-K`Vu(_MsgSFA#{2}jJERo}3Rxy;FbcVvx3x{8>7mz^zowKbH5i3l z%@Z0Y;mqFOrE>Qjk_J;DS92GK6ttQ$qsT2cN77&_KT7r%sH{*7*A_ulJ7gQ<|K`N7FR^sq=* z_9MDfgQ<}J7f%SFIDW4bWK{_t{LLujYJM}$kM=8GP*kgl32HD3xtb5Z^}}aVA1gli z8B-0WLaye7w%u`4-X!T(S5vCNRLIplw`F%qQ)84b`kA5zqmZjP$94tj(L9)H7gMNb zw~q|7=tslTuEB#wvxrS@7{z02VO#E8vgytUs&{NNOmg>>;ya|!VLHjuwUBPg#O6sf z+^;6YuUi3LVPoltu1&~Lk3?X1u%J(N`I8B|W=nI2vH!6;;}m7K2VsgqBps4}L7Dw+ z5H?v{KE zQ)h+JCM*hZ>qO!kI}H{s8-xvfByz~*HPp_kjU}=deX2c;#Dq4$Y9a1a`&J^UoLvL& zJn2gH#?2-hwWk&h$QVVB^*B-SeokGQM2}`xhBwhOq&YpK>8NEJfH?~8uw2P=)$8uL ztw&K8C;>xbV^DmIp<5PBQ8D2>>R@A_S0~Y>%h~~B!Z}zgq-f8HWEvQHRQcM&QTZV< ziMr)gAuLx2xy19F{2P+Vlw^9_ZHj`ilCx?XX0=VGqb=Smo3qCnaxQ^xu_%%l6V7hF z=ioq}2$-51O0n-Z^6Je?XwzMy!B_-OXZOc#ZA!?dUX7?@I-v8F>F{{(W$-`P293s= zV)?}ww45;=%**wO;i@otBkMBxp`D?y_>n+j;*IIx&RuBPg|@ioaYI@!^$0PkZiC8W zcZhCjV_ccKmRKH~NpcIrFgtIcGW5$NYJOuR)V2tgtUN=ocg{O-$WltxpT?^`Ga{w_ zugK05L0CF27+M_OuiTe!1;3UjV3h{PNWg7>tUL2Cxmz4a*AUd=nI>_@%n z-Y-pX`2%~UNsI4fFuT5Mzs?Ql(!&r3*X)f(E(Xx`<8Yeb8wUq_6v7+_b6PeuS=o;L zl26{gfpm3oE(||hLfRSm)0&-5Np)*irR%*X(Fqp!CGXO~B>!M5`l;G4c70e?dSY+_ zt+uQKfP1R)W?D4eHai{Kusf{3O!K2!4QsNyc~*v^XG7?Rh_~=D?~{_{$aCf+_gNj} zb(oLbw}0hJBez_KN!4zF@7Ctj`*}0mHRG^~ku!6UJI)CcN&9nU@UC@G#rt4=`#*uM zxx*^B(wJsAbZ{qZf3Z9J#RNM%?&E~a2kx_+{o=!)+}gS4@t&tNWFi)wnX1Yqo--e~uNvo< z6)Cv#-O37oQLq`E__-T>*ZMB$^YklOoaIh~D&HjuQ{N~jB@C;0f2_;bD(th{BvlSP zXFhQM_KO&@wq7`H*uIr82F@c^^`UiKiy`UMCH7fJBt6i3hjd5h0AV=_Ir4mJ-Ez`z zMhMPIU0U?zb}f3eKKllXP6xsmIWq^iv(zYuOwb#R?A#a_183E9)IG!Q6ytv!2G=s8 zmfaO-*MJa6xI;W|HNbczB(+?lNt6tF;Ymb{FdtF6uXmz2}?Anu7V5VZ^tn!EZWlIx%QgDXL zXyRC0C}sbc_!nz9AUJ9k9g%N5oIt!i8h|`uLdEtNf9nc!zr_E=qQe-tFS2=yx1`Z# zTexaG7?U3X?b0U+OhOy~#TpI@j+&VRypG79=Z~c?dXI;1r3s+HDCBCsTO3c%A7}te zv@4VR+#o8IPL?{((xD=2I4C%3W)AW?A|IYon~uqyBTXpjNi`UST+JE9AB*ZorO?<1 z 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 Date: Tue, 26 Dec 2017 23:38:05 +0100 Subject: [PATCH 18/54] Better performance --- .../src/main/java/com/jme3/anim/AnimClip.java | 4 +- .../src/main/java/com/jme3/anim/Armature.java | 2 +- .../src/main/java/com/jme3/anim/Joint.java | 11 +- .../main/java/com/jme3/anim/JointTrack.java | 9 +- .../main/java/com/jme3/anim/SpatialTrack.java | 11 +- .../java/com/jme3/anim/TransformTrack.java | 45 +----- .../src/main/java/com/jme3/math/Matrix4f.java | 135 +++++++++--------- .../main/java/com/jme3/math/Quaternion.java | 59 ++++++-- .../main/java/com/jme3/math/Transform.java | 11 +- .../jme3test/model/anim/TestHWSkinning.java | 9 +- .../model/anim/TestHWSkinningOld.java | 115 +++++++++++++++ 11 files changed, 269 insertions(+), 142 deletions(-) create mode 100644 jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinningOld.java diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java index 8bd64e0e1..6a23f4aff 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java @@ -66,7 +66,9 @@ public class AnimClip implements Tween, JmeCloneable, Savable { } for (Tween track : tracks.getArray()) { - track.interpolate(t); + if (t <= track.getLength()) { + track.interpolate(t); + } } return t <= length; } diff --git a/jme3-core/src/main/java/com/jme3/anim/Armature.java b/jme3-core/src/main/java/com/jme3/anim/Armature.java index 98b6299be..582dc8bce 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Armature.java +++ b/jme3-core/src/main/java/com/jme3/anim/Armature.java @@ -23,7 +23,7 @@ public class Armature implements JmeCloneable, Savable { * will cause it to go to the animated position. */ private transient Matrix4f[] skinningMatrixes; - private Class modelTransformClass = MatrixJointModelTransform.class; + private Class modelTransformClass = SeparateJointModelTransform.class; /** * Serialization only diff --git a/jme3-core/src/main/java/com/jme3/anim/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java index 6ff53f59c..267a5543a 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Joint.java +++ b/jme3-core/src/main/java/com/jme3/anim/Joint.java @@ -12,6 +12,7 @@ import com.jme3.util.clone.JmeCloneable; import java.io.IOException; import java.util.ArrayList; +import java.util.List; /** * A Joint is the basic component of an armature designed to perform skeletal animation @@ -21,7 +22,7 @@ public class Joint implements Savable, JmeCloneable { private String name; private Joint parent; - private ArrayList children = new ArrayList<>(); + private SafeArrayList children = new SafeArrayList<>(Joint.class); private Geometry targetGeometry; /** @@ -61,7 +62,7 @@ public class Joint implements Savable, JmeCloneable { public final void update() { this.updateModelTransforms(); - for (Joint child : children) { + for (Joint child : children.getArray()) { child.update(); } } @@ -135,7 +136,7 @@ public class Joint implements Savable, JmeCloneable { jointModelTransform.applyBindPose(localTransform, inverseModelBindMatrix, parent); updateModelTransforms(); - for (Joint child : children) { + for (Joint child : children.getArray()) { child.resetToBindPose(); } } @@ -197,7 +198,7 @@ public class Joint implements Savable, JmeCloneable { return parent; } - public ArrayList getChildren() { + public List getChildren() { return children; } @@ -291,7 +292,7 @@ public class Joint implements Savable, JmeCloneable { output.write(attachedNode, "attachedNode", null); output.write(targetGeometry, "targetGeometry", null); output.write(inverseModelBindMatrix, "inverseModelBindMatrix", new Matrix4f()); - output.writeSavableArrayList(children, "children", null); + output.writeSavableArrayList(new ArrayList(children), "children", null); } } diff --git a/jme3-core/src/main/java/com/jme3/anim/JointTrack.java b/jme3-core/src/main/java/com/jme3/anim/JointTrack.java index bd5f24025..2a24bcd84 100644 --- a/jme3-core/src/main/java/com/jme3/anim/JointTrack.java +++ b/jme3-core/src/main/java/com/jme3/anim/JointTrack.java @@ -66,11 +66,11 @@ public final class JointTrack extends TransformTrack implements JmeCloneable, Sa public JointTrack(Joint target, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { super(times, translations, rotations, scales); this.target = target; + this.defaultTransform = target.getLocalTransform(); } @Override public boolean interpolate(double t) { - setDefaultTransform(target.getLocalTransform()); boolean running = super.interpolate(t); Transform transform = getInterpolatedTransform(); target.setLocalTransform(transform); @@ -83,16 +83,11 @@ public final class JointTrack extends TransformTrack implements JmeCloneable, Sa @Override public Object jmeClone() { - try { - return super.clone(); - } catch (CloneNotSupportedException e) { - throw new RuntimeException("Error cloning", e); - } + return super.clone(); } @Override public void cloneFields(Cloner cloner, Object original) { - super.cloneFields(cloner, original); this.target = cloner.clone(target); } diff --git a/jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java b/jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java index b17235099..d796babfd 100644 --- a/jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java +++ b/jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java @@ -67,11 +67,12 @@ public final class SpatialTrack extends TransformTrack implements JmeCloneable, public SpatialTrack(Spatial target, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { super(times, translations, rotations, scales); this.target = target; + defaultTransform = target.getLocalTransform(); } @Override public boolean interpolate(double t) { - setDefaultTransform(target.getLocalTransform()); + boolean running = super.interpolate(t); Transform transform = getInterpolatedTransform(); target.setLocalTransform(transform); @@ -84,16 +85,12 @@ public final class SpatialTrack extends TransformTrack implements JmeCloneable, @Override public Object jmeClone() { - try { - return super.clone(); - } catch (CloneNotSupportedException e) { - throw new RuntimeException("Error cloning", e); - } + return super.clone(); } @Override public void cloneFields(Cloner cloner, Object original) { - super.cloneFields(cloner, original); + this.target = cloner.clone(target); } diff --git a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java index 5e0064ac5..bf06379ae 100644 --- a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java +++ b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java @@ -37,8 +37,6 @@ import com.jme3.animation.CompactQuaternionArray; import com.jme3.animation.CompactVector3Array; import com.jme3.export.*; import com.jme3.math.*; -import com.jme3.util.clone.Cloner; -import com.jme3.util.clone.JmeCloneable; import java.io.IOException; @@ -47,7 +45,7 @@ import java.io.IOException; * * @author Rémy Bouquet */ -public abstract class TransformTrack implements Tween, JmeCloneable, Savable { +public abstract class TransformTrack implements Tween, Cloneable, Savable { private double length; @@ -58,7 +56,7 @@ public abstract class TransformTrack implements Tween, JmeCloneable, Savable { private CompactQuaternionArray rotations; private CompactVector3Array scales; private Transform transform = new Transform(); - private Transform defaultTransform = new Transform(); + protected Transform defaultTransform = new Transform(); private FrameInterpolator interpolator = FrameInterpolator.DEFAULT; private float[] times; @@ -283,10 +281,6 @@ public abstract class TransformTrack implements Tween, JmeCloneable, Savable { return transform; } - public void setDefaultTransform(Transform transforms) { - defaultTransform.set(transforms); - } - public void setFrameInterpolator(FrameInterpolator interpolator) { this.interpolator = interpolator; } @@ -311,7 +305,7 @@ public abstract class TransformTrack implements Tween, JmeCloneable, Savable { } @Override - public Object jmeClone() { + public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { @@ -319,37 +313,4 @@ public abstract class TransformTrack implements Tween, JmeCloneable, Savable { } } - @Override - public void cloneFields(Cloner cloner, Object original) { - int tablesLength = times.length; - - setTimes(this.times.clone()); - if (translations != null) { - Vector3f[] sourceTranslations = this.getTranslations(); - Vector3f[] translations = new Vector3f[tablesLength]; - for (int i = 0; i < tablesLength; ++i) { - translations[i] = sourceTranslations[i].clone(); - } - setKeyframesTranslation(translations); - } - if (rotations != null) { - Quaternion[] sourceRotations = this.getRotations(); - Quaternion[] rotations = new Quaternion[tablesLength]; - for (int i = 0; i < tablesLength; ++i) { - rotations[i] = sourceRotations[i].clone(); - } - setKeyframesRotation(rotations); - } - - if (scales != null) { - Vector3f[] sourceScales = this.getScales(); - Vector3f[] scales = new Vector3f[tablesLength]; - for (int i = 0; i < tablesLength; ++i) { - scales[i] = sourceScales[i].clone(); - } - setKeyframesScale(scales); - } - - setFrameInterpolator(this.interpolator); - } } diff --git a/jme3-core/src/main/java/com/jme3/math/Matrix4f.java b/jme3-core/src/main/java/com/jme3/math/Matrix4f.java index 159e39932..6b260ee31 100644 --- a/jme3-core/src/main/java/com/jme3/math/Matrix4f.java +++ b/jme3-core/src/main/java/com/jme3/math/Matrix4f.java @@ -34,6 +34,7 @@ package com.jme3.math; import com.jme3.export.*; import com.jme3.util.BufferUtils; import com.jme3.util.TempVars; + import java.io.IOException; import java.nio.FloatBuffer; import java.util.logging.Logger; @@ -1023,96 +1024,95 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable store = new Matrix4f(); } - float temp00, temp01, temp02, temp03; - float temp10, temp11, temp12, temp13; - float temp20, temp21, temp22, temp23; - float temp30, temp31, temp32, temp33; + TempVars v = TempVars.get(); + float[] m = v.matrixWrite; - temp00 = m00 * in2.m00 + m[0] = m00 * in2.m00 + m01 * in2.m10 + m02 * in2.m20 + m03 * in2.m30; - temp01 = m00 * in2.m01 + m[1] = m00 * in2.m01 + m01 * in2.m11 + m02 * in2.m21 + m03 * in2.m31; - temp02 = m00 * in2.m02 + m[2] = m00 * in2.m02 + m01 * in2.m12 + m02 * in2.m22 + m03 * in2.m32; - temp03 = m00 * in2.m03 + m[3] = m00 * in2.m03 + m01 * in2.m13 + m02 * in2.m23 + m03 * in2.m33; - temp10 = m10 * in2.m00 + m[4] = m10 * in2.m00 + m11 * in2.m10 + m12 * in2.m20 + m13 * in2.m30; - temp11 = m10 * in2.m01 + m[5] = m10 * in2.m01 + m11 * in2.m11 + m12 * in2.m21 + m13 * in2.m31; - temp12 = m10 * in2.m02 + m[6] = m10 * in2.m02 + m11 * in2.m12 + m12 * in2.m22 + m13 * in2.m32; - temp13 = m10 * in2.m03 + m[7] = m10 * in2.m03 + m11 * in2.m13 + m12 * in2.m23 + m13 * in2.m33; - temp20 = m20 * in2.m00 + m[8] = m20 * in2.m00 + m21 * in2.m10 + m22 * in2.m20 + m23 * in2.m30; - temp21 = m20 * in2.m01 + m[9] = m20 * in2.m01 + m21 * in2.m11 + m22 * in2.m21 + m23 * in2.m31; - temp22 = m20 * in2.m02 + m[10] = m20 * in2.m02 + m21 * in2.m12 + m22 * in2.m22 + m23 * in2.m32; - temp23 = m20 * in2.m03 + m[11] = m20 * in2.m03 + m21 * in2.m13 + m22 * in2.m23 + m23 * in2.m33; - temp30 = m30 * in2.m00 + m[12] = m30 * in2.m00 + m31 * in2.m10 + m32 * in2.m20 + m33 * in2.m30; - temp31 = m30 * in2.m01 + m[13] = m30 * in2.m01 + m31 * in2.m11 + m32 * in2.m21 + m33 * in2.m31; - temp32 = m30 * in2.m02 + m[14] = m30 * in2.m02 + m31 * in2.m12 + m32 * in2.m22 + m33 * in2.m32; - temp33 = m30 * in2.m03 + m[15] = m30 * in2.m03 + m31 * in2.m13 + m32 * in2.m23 + m33 * in2.m33; - store.m00 = temp00; - store.m01 = temp01; - store.m02 = temp02; - store.m03 = temp03; - store.m10 = temp10; - store.m11 = temp11; - store.m12 = temp12; - store.m13 = temp13; - store.m20 = temp20; - store.m21 = temp21; - store.m22 = temp22; - store.m23 = temp23; - store.m30 = temp30; - store.m31 = temp31; - store.m32 = temp32; - store.m33 = temp33; + store.m00 = m[0]; + store.m01 = m[1]; + store.m02 = m[2]; + store.m03 = m[3]; + store.m10 = m[4]; + store.m11 = m[5]; + store.m12 = m[6]; + store.m13 = m[7]; + store.m20 = m[8]; + store.m21 = m[9]; + store.m22 = m[10]; + store.m23 = m[11]; + store.m30 = m[12]; + store.m31 = m[13]; + store.m32 = m[14]; + store.m33 = m[15]; + v.release(); return store; } @@ -1709,8 +1709,8 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable return new Vector3f(m03, m13, m23); } - public void toTranslationVector(Vector3f vector) { - vector.set(m03, m13, m23); + public Vector3f toTranslationVector(Vector3f vector) { + return vector.set(m03, m13, m23); } public Quaternion toRotationQuat() { @@ -1719,8 +1719,9 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable return quat; } - public void toRotationQuat(Quaternion q) { - q.fromRotationMatrix(toRotationMatrix()); + public Quaternion toRotationQuat(Quaternion q) { + return q.fromRotationMatrix(m00, m01, m02, m10, + m11, m12, m20, m21, m22); } public Matrix3f toRotationMatrix() { @@ -1753,15 +1754,16 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable /** * Retreives the scale vector from the matrix and stores it into a given * vector. - * - * @param the - * vector where the scale will be stored + * + * @param store the vector where the scale will be stored + * @return the store vector */ - public void toScaleVector(Vector3f vector) { + public Vector3f toScaleVector(Vector3f store) { float scaleX = (float) Math.sqrt(m00 * m00 + m10 * m10 + m20 * m20); float scaleY = (float) Math.sqrt(m01 * m01 + m11 * m11 + m21 * m21); float scaleZ = (float) Math.sqrt(m02 * m02 + m12 * m12 + m22 * m22); - vector.set(scaleX, scaleY, scaleZ); + store.set(scaleX, scaleY, scaleZ); + return store; } /** @@ -1775,25 +1777,30 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable * the Z scale */ public void setScale(float x, float y, float z) { - TempVars vars = TempVars.get(); - vars.vect1.set(m00, m10, m20); - vars.vect1.normalizeLocal().multLocal(x); - m00 = vars.vect1.x; - m10 = vars.vect1.y; - m20 = vars.vect1.z; - - vars.vect1.set(m01, m11, m21); - vars.vect1.normalizeLocal().multLocal(y); - m01 = vars.vect1.x; - m11 = vars.vect1.y; - m21 = vars.vect1.z; - - vars.vect1.set(m02, m12, m22); - vars.vect1.normalizeLocal().multLocal(z); - m02 = vars.vect1.x; - m12 = vars.vect1.y; - m22 = vars.vect1.z; - vars.release(); + + float length = m00 * m00 + m10 * m10 + m20 * m20; + if (length != 0f) { + length = length == 1 ? x : (x / FastMath.sqrt(length)); + m00 *= length; + m10 *= length; + m20 *= length; + } + + length = m01 * m01 + m11 * m11 + m21 * m21; + if (length != 0f) { + length = length == 1 ? y : (y / FastMath.sqrt(length)); + m01 *= length; + m11 *= length; + m21 *= length; + } + + length = m02 * m02 + m12 * m12 + m22 * m22; + if (length != 0f) { + length = length == 1 ? z : (z / FastMath.sqrt(length)); + m02 *= length; + m12 *= length; + m22 *= length; + } } /** diff --git a/jme3-core/src/main/java/com/jme3/math/Quaternion.java b/jme3-core/src/main/java/com/jme3/math/Quaternion.java index 89c7d8647..d592640e1 100644 --- a/jme3-core/src/main/java/com/jme3/math/Quaternion.java +++ b/jme3-core/src/main/java/com/jme3/math/Quaternion.java @@ -33,10 +33,8 @@ package com.jme3.math; import com.jme3.export.*; import com.jme3.util.TempVars; -import java.io.Externalizable; -import java.io.IOException; -import java.io.ObjectInput; -import java.io.ObjectOutput; + +import java.io.*; import java.util.logging.Logger; /** @@ -452,11 +450,56 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl return result; } + /** + * toTransformMatrix converts this quaternion to a transform + * matrix. The result is stored in result. + * Note this method won't preserve the scale of the given matrix. + * + * @param store The Matrix3f to store the result in. + * @return the transform matrix with the rotation representation of this quaternion. + */ + public Matrix4f toTransformMatrix(Matrix4f store) { + + float norm = norm(); + // we explicitly test norm against one here, saving a division + // at the cost of a test and branch. Is it worth it? + float s = (norm == 1f) ? 2f : (norm > 0f) ? 2f / norm : 0; + + // compute xs/ys/zs first to save 6 multiplications, since xs/ys/zs + // will be used 2-4 times each. + float xs = x * s; + float ys = y * s; + float zs = z * s; + float xx = x * xs; + float xy = x * ys; + float xz = x * zs; + float xw = w * xs; + float yy = y * ys; + float yz = y * zs; + float yw = w * ys; + float zz = z * zs; + float zw = w * zs; + + // using s=2/norm (instead of 1/norm) saves 9 multiplications by 2 here + store.m00 = 1 - (yy + zz); + store.m01 = (xy - zw); + store.m02 = (xz + yw); + store.m10 = (xy + zw); + store.m11 = 1 - (xx + zz); + store.m12 = (yz - xw); + store.m20 = (xz - yw); + store.m21 = (yz + xw); + store.m22 = 1 - (xx + yy); + + return store; + } + /** * toRotationMatrix converts this quaternion to a rotational * matrix. The result is stored in result. 4th row and 4th column values are * untouched. Note: the result is created from a normalized version of this quat. - * + * Note that this method will preserve the scale of the given matrix + * * @param result * The Matrix4f to store the result in. * @return the rotation matrix representation of this quaternion. @@ -464,7 +507,7 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl public Matrix4f toRotationMatrix(Matrix4f result) { TempVars tempv = TempVars.get(); Vector3f originalScale = tempv.vect1; - + result.toScaleVector(originalScale); result.setScale(1, 1, 1); float norm = norm(); @@ -499,9 +542,9 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl result.m22 = 1 - (xx + yy); result.setScale(originalScale); - + tempv.release(); - + return result; } diff --git a/jme3-core/src/main/java/com/jme3/math/Transform.java b/jme3-core/src/main/java/com/jme3/math/Transform.java index 6c8795a4a..41e911bac 100644 --- a/jme3-core/src/main/java/com/jme3/math/Transform.java +++ b/jme3-core/src/main/java/com/jme3/math/Transform.java @@ -32,6 +32,7 @@ package com.jme3.math; import com.jme3.export.*; +import com.jme3.util.TempVars; import java.io.IOException; @@ -267,15 +268,17 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable store = new Matrix4f(); } store.setTranslation(translation); - store.setRotationQuaternion(rot); + rot.toTransformMatrix(store); store.setScale(scale); return store; } public void fromTransformMatrix(Matrix4f mat) { - translation.set(mat.toTranslationVector()); - rot.set(mat.toRotationQuat()); - scale.set(mat.toScaleVector()); + TempVars vars = TempVars.get(); + translation.set(mat.toTranslationVector(vars.vect1)); + rot.set(mat.toRotationQuat(vars.quat1)); + scale.set(mat.toScaleVector(vars.vect2)); + vars.release(); } public Transform invert() { diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java index 980d8ca59..9e92cd35d 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java @@ -50,7 +50,7 @@ public class TestHWSkinning extends SimpleApplication implements ActionListener{ private AnimComposer composer; private String[] animNames = {"Dodge", "Walk", "pull", "push"}; - private final static int SIZE = 10; + private final static int SIZE = 50; private boolean hwSkinningEnable = true; private List skControls = new ArrayList(); private BitmapText hwsText; @@ -63,8 +63,11 @@ public class TestHWSkinning extends SimpleApplication implements ActionListener{ @Override public void simpleInitApp() { flyCam.setMoveSpeed(10f); - cam.setLocation(new Vector3f(3.8664846f, 6.2704787f, 9.664585f)); - cam.setRotation(new Quaternion(-0.054774776f, 0.94064945f, -0.27974048f, -0.18418397f)); + flyCam.setDragToRotate(true); + setPauseOnLostFocus(false); + cam.setLocation(new Vector3f(24.746134f, 13.081396f, 32.72753f)); + cam.setRotation(new Quaternion(-0.06867662f, 0.92435044f, -0.19981281f, -0.31770203f)); + makeHudText(); DirectionalLight dl = new DirectionalLight(); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinningOld.java b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinningOld.java new file mode 100644 index 000000000..f0fa27d8f --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinningOld.java @@ -0,0 +1,115 @@ +/* + * 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 jme3test.model.anim; + +import com.jme3.animation.*; +import com.jme3.app.SimpleApplication; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.light.DirectionalLight; +import com.jme3.math.*; +import com.jme3.scene.Spatial; + +import java.util.ArrayList; +import java.util.List; + +public class TestHWSkinningOld extends SimpleApplication implements ActionListener { + + private AnimChannel channel; + private AnimControl control; + private String[] animNames = {"Dodge", "Walk", "pull", "push"}; + private final static int SIZE = 50; + private boolean hwSkinningEnable = true; + private List skControls = new ArrayList(); + private BitmapText hwsText; + + public static void main(String[] args) { + TestHWSkinningOld app = new TestHWSkinningOld(); + app.start(); + } + + @Override + public void simpleInitApp() { + flyCam.setMoveSpeed(10f); + flyCam.setDragToRotate(true); + setPauseOnLostFocus(false); + cam.setLocation(new Vector3f(24.746134f, 13.081396f, 32.72753f)); + cam.setRotation(new Quaternion(-0.06867662f, 0.92435044f, -0.19981281f, -0.31770203f)); + makeHudText(); + + DirectionalLight dl = new DirectionalLight(); + dl.setDirection(new Vector3f(-0.1f, -0.7f, -1).normalizeLocal()); + dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f)); + rootNode.addLight(dl); + + for (int i = 0; i < SIZE; i++) { + for (int j = 0; j < SIZE; j++) { + Spatial model = (Spatial) assetManager.loadModel("Models/Oto/OtoOldAnim.j3o"); + model.setLocalScale(0.1f); + model.setLocalTranslation(i - SIZE / 2, 0, j - SIZE / 2); + control = model.getControl(AnimControl.class); + + channel = control.createChannel(); + channel.setAnim(animNames[(i + j) % 4]); + SkeletonControl skeletonControl = model.getControl(SkeletonControl.class); + skeletonControl.setHardwareSkinningPreferred(hwSkinningEnable); + skControls.add(skeletonControl); + rootNode.attachChild(model); + } + } + + inputManager.addListener(this, "toggleHWS"); + inputManager.addMapping("toggleHWS", new KeyTrigger(KeyInput.KEY_SPACE)); + } + + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed && name.equals("toggleHWS")) { + hwSkinningEnable = !hwSkinningEnable; + for (SkeletonControl control : skControls) { + control.setHardwareSkinningPreferred(hwSkinningEnable); + hwsText.setText("HWS : " + hwSkinningEnable); + } + } + } + + private void makeHudText() { + guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); + hwsText = new BitmapText(guiFont, false); + hwsText.setSize(guiFont.getCharSet().getRenderedSize()); + hwsText.setText("HWS : " + hwSkinningEnable); + hwsText.setLocalTranslation(0, cam.getHeight(), 0); + guiNode.attachChild(hwsText); + } +} \ No newline at end of file From 05e907acca36a9c81b545c7e27a0dbab2ebcf4cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81my=20Bouquet?= Date: Sat, 30 Dec 2017 17:33:35 +0100 Subject: [PATCH 19/54] Added the base of the blending system. Smooth transition between anims --- .../src/main/java/com/jme3/anim/AnimClip.java | 63 ++++------ .../main/java/com/jme3/anim/AnimComposer.java | 59 ++++++++-- .../src/main/java/com/jme3/anim/Joint.java | 3 +- .../main/java/com/jme3/anim/JointTrack.java | 108 ----------------- .../main/java/com/jme3/anim/SpatialTrack.java | 111 ------------------ .../java/com/jme3/anim/TransformTrack.java | 46 +++++--- .../com/jme3/anim/tween/AnimClipTween.java | 99 ++++++++++++++++ .../com/jme3/anim/tween/action/Action.java | 64 ++++++++++ .../anim/tween/action/SequenceAction.java | 75 ++++++++++++ .../jme3/anim/util/AnimMigrationUtils.java | 25 ++-- .../com/jme3/anim/util/HasLocalTransform.java | 10 ++ .../java/com/jme3/anim/util/Weighted.java | 11 ++ .../main/java/com/jme3/math/Transform.java | 2 +- .../src/main/java/com/jme3/scene/Spatial.java | 5 +- .../MatDefs/ShaderNodes/Misc/Dashed100.frag | 4 +- .../java/jme3test/export/TestOgreConvert.java | 2 +- .../java/jme3test/model/TestGltfLoading.java | 8 +- .../model/anim/TestAnimMigration.java | 30 +++-- .../model/anim/TestAnimSerialization.java | 6 +- .../jme3test/model/anim/TestArmature.java | 12 +- .../model/anim/TestBaseAnimSerialization.java | 12 +- .../jme3test/model/anim/TestHWSkinning.java | 8 +- .../model/anim/TestModelExportingCloning.java | 6 +- .../jme3/scene/plugins/gltf/GltfLoader.java | 17 +-- .../scene/plugins/ogre/SkeletonLoader.java | 24 ++-- 25 files changed, 452 insertions(+), 358 deletions(-) delete mode 100644 jme3-core/src/main/java/com/jme3/anim/JointTrack.java delete mode 100644 jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/action/SequenceAction.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/util/Weighted.java diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java index 6a23f4aff..96cde46a2 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimClip.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimClip.java @@ -11,12 +11,12 @@ import java.io.IOException; /** * Created by Nehon on 20/12/2017. */ -public class AnimClip implements Tween, JmeCloneable, Savable { +public class AnimClip implements JmeCloneable, Savable { private String name; private double length; - private SafeArrayList tracks = new SafeArrayList<>(Tween.class); + private TransformTrack[] tracks; public AnimClip() { } @@ -25,26 +25,11 @@ public class AnimClip implements Tween, JmeCloneable, Savable { this.name = name; } - public void setTracks(Tween[] tracks) { - for (Tween track : tracks) { - addTrack(track); - } - } - - public void addTrack(Tween track) { - tracks.add(track); - if (track.getLength() > length) { - length = track.getLength(); - } - } - - public void removeTrack(Tween track) { - if (tracks.remove(track)) { - length = 0; - for (Tween t : tracks.getArray()) { - if (t.getLength() > length) { - length = t.getLength(); - } + public void setTracks(TransformTrack[] tracks) { + this.tracks = tracks; + for (TransformTrack track : tracks) { + if (track.getLength() > length) { + length = track.getLength(); } } } @@ -53,24 +38,14 @@ public class AnimClip implements Tween, JmeCloneable, Savable { return name; } - @Override + public double getLength() { return length; } - @Override - public boolean interpolate(double t) { - // Sanity check the inputs - if (t < 0) { - return true; - } - for (Tween track : tracks.getArray()) { - if (t <= track.getLength()) { - track.interpolate(t); - } - } - return t <= length; + public TransformTrack[] getTracks() { + return tracks; } @Override @@ -84,9 +59,9 @@ public class AnimClip implements Tween, JmeCloneable, Savable { @Override public void cloneFields(Cloner cloner, Object original) { - SafeArrayList newTracks = new SafeArrayList<>(Tween.class); - for (Tween track : tracks) { - newTracks.add(cloner.clone(track)); + TransformTrack[] newTracks = new TransformTrack[tracks.length]; + for (int i = 0; i < tracks.length; i++) { + newTracks[i] = (cloner.clone(tracks[i])); } this.tracks = newTracks; } @@ -95,7 +70,7 @@ public class AnimClip implements Tween, JmeCloneable, Savable { public void write(JmeExporter ex) throws IOException { OutputCapsule oc = ex.getCapsule(this); oc.write(name, "name", null); - oc.write(tracks.getArray(), "tracks", null); + oc.write(tracks, "tracks", null); } @@ -105,9 +80,13 @@ public class AnimClip implements Tween, JmeCloneable, Savable { name = ic.readString("name", null); Savable[] arr = ic.readSavableArray("tracks", null); if (arr != null) { - tracks = new SafeArrayList<>(Tween.class); - for (Savable savable : arr) { - addTrack((Tween) savable); + tracks = new TransformTrack[arr.length]; + for (int i = 0; i < arr.length; i++) { + TransformTrack t = (TransformTrack) arr[i]; + tracks[i] = t; + if (t.getLength() > length) { + length = t.getLength(); + } } } } diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index 731f92a19..1515136ef 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -1,5 +1,9 @@ package com.jme3.anim; +import com.jme3.anim.tween.AnimClipTween; +import com.jme3.anim.tween.Tween; +import com.jme3.anim.tween.action.Action; +import com.jme3.anim.tween.action.SequenceAction; import com.jme3.export.*; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; @@ -16,7 +20,8 @@ public class AnimComposer extends AbstractControl { private Map animClipMap = new HashMap<>(); - private AnimClip currentAnimClip; + private Action currentAction; + private Map actions = new HashMap<>(); private float time; /** @@ -54,16 +59,45 @@ public class AnimComposer extends AbstractControl { animClipMap.remove(anim.getName()); } - public void setCurrentAnimClip(String name) { - currentAnimClip = animClipMap.get(name); + public void setCurrentAction(String name) { + Action action = action(name); + if (currentAction != null) { + currentAction.reset(); + } + currentAction = action; time = 0; - if (currentAnimClip == null) { - throw new IllegalArgumentException("Unknown clip " + name); + } + + public Action action(String name) { + Action action = actions.get(name); + if (action == null) { + AnimClipTween tween = tweenFromClip(name); + action = new SequenceAction(tween); + actions.put(name, action); + } + return action; + } + + public AnimClipTween tweenFromClip(String clipName) { + AnimClip clip = animClipMap.get(clipName); + if (clip == null) { + throw new IllegalArgumentException("Cannot find clip named " + clipName); } + return new AnimClipTween(clip); + } + + public SequenceAction actionSequence(String name, Tween... tweens) { + SequenceAction action = new SequenceAction(tweens); + actions.put(name, action); + return action; + } + + public Action actionBlended(String name, Tween... tweens) { + return null; } public void reset() { - currentAnimClip = null; + currentAction = null; time = 0; } @@ -77,11 +111,11 @@ public class AnimComposer extends AbstractControl { @Override protected void controlUpdate(float tpf) { - if (currentAnimClip != null) { + if (currentAction != null) { time += tpf; - boolean running = currentAnimClip.interpolate(time); + boolean running = currentAction.interpolate(time); if (!running) { - time -= currentAnimClip.getLength(); + time -= currentAction.getLength(); } } } @@ -108,6 +142,11 @@ public class AnimComposer extends AbstractControl { for (String key : animClipMap.keySet()) { clips.put(key, cloner.clone(animClipMap.get(key))); } + Map act = new HashMap<>(); + for (String key : actions.keySet()) { + act.put(key, cloner.clone(actions.get(key))); + } + actions = act; animClipMap = clips; } @@ -116,6 +155,7 @@ public class AnimComposer extends AbstractControl { super.read(im); InputCapsule ic = im.getCapsule(this); animClipMap = (Map) ic.readStringSavableMap("animClipMap", new HashMap()); + actions = (Map) ic.readStringSavableMap("actions", new HashMap()); } @Override @@ -123,5 +163,6 @@ public class AnimComposer extends AbstractControl { super.write(ex); OutputCapsule oc = ex.getCapsule(this); oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap()); + oc.writeStringSavableMap(actions, "actions", new HashMap()); } } diff --git a/jme3-core/src/main/java/com/jme3/anim/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java index 267a5543a..eaa7ac145 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Joint.java +++ b/jme3-core/src/main/java/com/jme3/anim/Joint.java @@ -1,5 +1,6 @@ package com.jme3.anim; +import com.jme3.anim.util.HasLocalTransform; import com.jme3.anim.util.JointModelTransform; import com.jme3.export.*; import com.jme3.material.MatParamOverride; @@ -18,7 +19,7 @@ import java.util.List; * A Joint is the basic component of an armature designed to perform skeletal animation * Created by Nehon on 15/12/2017. */ -public class Joint implements Savable, JmeCloneable { +public class Joint implements Savable, JmeCloneable, HasLocalTransform { private String name; private Joint parent; diff --git a/jme3-core/src/main/java/com/jme3/anim/JointTrack.java b/jme3-core/src/main/java/com/jme3/anim/JointTrack.java deleted file mode 100644 index 2a24bcd84..000000000 --- a/jme3-core/src/main/java/com/jme3/anim/JointTrack.java +++ /dev/null @@ -1,108 +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.anim; - -import com.jme3.export.*; -import com.jme3.math.*; -import com.jme3.util.clone.Cloner; -import com.jme3.util.clone.JmeCloneable; - -import java.io.IOException; - -/** - * Contains a list of transforms and times for each keyframe. - * - * @author Rémy Bouquet - */ -public final class JointTrack extends TransformTrack implements JmeCloneable, Savable { - - private Joint target; - - /** - * Serialization-only. Do not use. - */ - public JointTrack() { - super(); - } - - /** - * Creates a joint track for the given joint index - * - * @param target The Joint target of this track - * @param times a float array with the time of each frame - * @param translations the translation of the bone for each frame - * @param rotations the rotation of the bone for each frame - * @param scales the scale of the bone for each frame - */ - public JointTrack(Joint target, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { - super(times, translations, rotations, scales); - this.target = target; - this.defaultTransform = target.getLocalTransform(); - } - - @Override - public boolean interpolate(double t) { - boolean running = super.interpolate(t); - Transform transform = getInterpolatedTransform(); - target.setLocalTransform(transform); - return running; - } - - public void setTarget(Joint target) { - this.target = target; - } - - @Override - public Object jmeClone() { - return super.clone(); - } - - @Override - public void cloneFields(Cloner cloner, Object original) { - this.target = cloner.clone(target); - } - - @Override - public void write(JmeExporter ex) throws IOException { - super.write(ex); - OutputCapsule oc = ex.getCapsule(this); - oc.write(target, "target", null); - } - - @Override - public void read(JmeImporter im) throws IOException { - super.read(im); - InputCapsule ic = im.getCapsule(this); - target = (Joint) ic.readSavable("target", null); - } - -} diff --git a/jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java b/jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java deleted file mode 100644 index d796babfd..000000000 --- a/jme3-core/src/main/java/com/jme3/anim/SpatialTrack.java +++ /dev/null @@ -1,111 +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.anim; - -import com.jme3.export.*; -import com.jme3.math.*; -import com.jme3.scene.Spatial; -import com.jme3.util.clone.Cloner; -import com.jme3.util.clone.JmeCloneable; - -import java.io.IOException; - -/** - * Contains a list of transforms and times for each keyframe. - * - * @author Rémy Bouquet - */ -public final class SpatialTrack extends TransformTrack implements JmeCloneable, Savable { - - private Spatial target; - - /** - * Serialization-only. Do not use. - */ - public SpatialTrack() { - super(); - } - - /** - * Creates a spatial track for the given Spatial - * - * @param target The Spatial target of this track - * @param times a float array with the time of each frame - * @param translations the translation of the bone for each frame - * @param rotations the rotation of the bone for each frame - * @param scales the scale of the bone for each frame - */ - public SpatialTrack(Spatial target, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { - super(times, translations, rotations, scales); - this.target = target; - defaultTransform = target.getLocalTransform(); - } - - @Override - public boolean interpolate(double t) { - - boolean running = super.interpolate(t); - Transform transform = getInterpolatedTransform(); - target.setLocalTransform(transform); - return running; - } - - public void setTarget(Spatial target) { - this.target = target; - } - - @Override - public Object jmeClone() { - return super.clone(); - } - - @Override - public void cloneFields(Cloner cloner, Object original) { - - this.target = cloner.clone(target); - } - - @Override - public void write(JmeExporter ex) throws IOException { - super.write(ex); - OutputCapsule oc = ex.getCapsule(this); - oc.write(target, "target", null); - } - - @Override - public void read(JmeImporter im) throws IOException { - super.read(im); - InputCapsule ic = im.getCapsule(this); - target = (Spatial) ic.readSavable("target", null); - } - -} diff --git a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java index bf06379ae..cc5bc317b 100644 --- a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java +++ b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java @@ -33,10 +33,13 @@ package com.jme3.anim; import com.jme3.anim.interpolator.FrameInterpolator; import com.jme3.anim.tween.Tween; +import com.jme3.anim.util.HasLocalTransform; import com.jme3.animation.CompactQuaternionArray; import com.jme3.animation.CompactVector3Array; import com.jme3.export.*; import com.jme3.math.*; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; import java.io.IOException; @@ -45,9 +48,10 @@ import java.io.IOException; * * @author Rémy Bouquet */ -public abstract class TransformTrack implements Tween, Cloneable, Savable { +public class TransformTrack implements JmeCloneable, Savable { private double length; + private HasLocalTransform target; /** * Transforms and times for track. @@ -55,8 +59,6 @@ public abstract class TransformTrack implements Tween, Cloneable, Savable { private CompactVector3Array translations; private CompactQuaternionArray rotations; private CompactVector3Array scales; - private Transform transform = new Transform(); - protected Transform defaultTransform = new Transform(); private FrameInterpolator interpolator = FrameInterpolator.DEFAULT; private float[] times; @@ -74,7 +76,8 @@ public abstract class TransformTrack implements Tween, Cloneable, Savable { * @param rotations the rotation of the bone for each frame * @param scales the scale of the bone for each frame */ - public TransformTrack(float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { + public TransformTrack(HasLocalTransform target, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { + this.target = target; this.setKeyframes(times, translations, rotations, scales); } @@ -216,16 +219,13 @@ public abstract class TransformTrack implements Tween, Cloneable, Savable { } } - @Override public double getLength() { return length; } - @Override - public boolean interpolate(double t) { + public void getTransformAtTime(double t, Transform transform) { float time = (float) t; - transform.set(defaultTransform); int lastFrame = times.length - 1; if (time < 0 || lastFrame == 0) { if (translations != null) { @@ -237,7 +237,7 @@ public abstract class TransformTrack implements Tween, Cloneable, Savable { if (scales != null) { scales.get(0, transform.getScale()); } - return true; + return; } int startFrame = 0; @@ -272,17 +272,18 @@ public abstract class TransformTrack implements Tween, Cloneable, Savable { if (scales != null) { transform.setScale(interpolated.getScale()); } - - return time < length; } + public void setFrameInterpolator(FrameInterpolator interpolator) { + this.interpolator = interpolator; + } - public Transform getInterpolatedTransform() { - return transform; + public HasLocalTransform getTarget() { + return target; } - public void setFrameInterpolator(FrameInterpolator interpolator) { - this.interpolator = interpolator; + public void setTarget(HasLocalTransform target) { + this.target = target; } @Override @@ -292,6 +293,7 @@ public abstract class TransformTrack implements Tween, Cloneable, Savable { oc.write(rotations, "rotations", null); oc.write(times, "times", null); oc.write(scales, "scales", null); + oc.write(target, "target", null); } @Override @@ -301,16 +303,22 @@ public abstract class TransformTrack implements Tween, Cloneable, Savable { rotations = (CompactQuaternionArray) ic.readSavable("rotations", null); times = ic.readFloatArray("times", null); scales = (CompactVector3Array) ic.readSavable("scales", null); + target = (Joint) ic.readSavable("target", null); setTimes(times); } @Override - public Object clone() { + public Object jmeClone() { try { - return super.clone(); - } catch (CloneNotSupportedException e) { - throw new RuntimeException("Error cloning", e); + TransformTrack clone = (TransformTrack) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); } } + @Override + public void cloneFields(Cloner cloner, Object original) { + this.target = cloner.clone(target); + } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java b/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java new file mode 100644 index 000000000..384abe31e --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java @@ -0,0 +1,99 @@ +package com.jme3.anim.tween; + +import com.jme3.anim.AnimClip; +import com.jme3.anim.TransformTrack; +import com.jme3.anim.tween.action.Action; +import com.jme3.anim.util.HasLocalTransform; +import com.jme3.anim.util.Weighted; +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; +import com.jme3.math.Transform; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; + +public class AnimClipTween implements Tween, Weighted, JmeCloneable { + + private AnimClip clip; + private Transform transform = new Transform(); + private float weight = 1f; + private Action parentAction; + + public AnimClipTween() { + } + + public AnimClipTween(AnimClip clip) { + this.clip = clip; + } + + @Override + public double getLength() { + return clip.getLength(); + } + + @Override + public boolean interpolate(double t) { + // Sanity check the inputs + if (t < 0) { + return true; + } + if (parentAction != null) { + weight = parentAction.getWeightForTween(this); + } + TransformTrack[] tracks = clip.getTracks(); + for (TransformTrack track : tracks) { + HasLocalTransform target = track.getTarget(); + transform.set(target.getLocalTransform()); + track.getTransformAtTime(t, transform); + + if (weight == 1f) { + target.setLocalTransform(transform); + } else { + Transform tr = target.getLocalTransform(); + tr.interpolateTransforms(tr, transform, weight); + target.setLocalTransform(tr); + } + } + return t < clip.getLength(); + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule oc = ex.getCapsule(this); + oc.write(clip, "clip", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + clip = (AnimClip) ic.readSavable("clip", null); + } + + @Override + public Object jmeClone() { + try { + AnimClipTween clone = (AnimClipTween) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + clip = cloner.clone(clip); + } + +// @Override +// public void setWeight(float weight) { +// this.weight = weight; +// } + + @Override + public void setParentAction(Action action) { + this.parentAction = action; + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java new file mode 100644 index 000000000..9c7037715 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java @@ -0,0 +1,64 @@ +package com.jme3.anim.tween.action; + +import com.jme3.anim.tween.Tween; +import com.jme3.anim.util.Weighted; +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; + +import java.io.IOException; + +public abstract class Action implements Tween, Weighted { + + protected Tween[] tweens; + protected float weight = 1; + protected double length; + protected Action parentAction; + + protected Action(Tween... tweens) { + this.tweens = tweens; + for (Tween tween : tweens) { + if (tween instanceof Weighted) { + ((Weighted) tween).setParentAction(this); + } + } + } + + @Override + public double getLength() { + return length; + } + + @Override + public boolean interpolate(double t) { + if (parentAction != null) { + weight = parentAction.getWeightForTween(this); + } + + return doInterpolate(t); + } + + public abstract float getWeightForTween(Tween tween); + + public abstract boolean doInterpolate(double t); + + public abstract void reset(); + + @Override + public void setParentAction(Action parentAction) { + this.parentAction = parentAction; + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + tweens = (Tween[]) ic.readSavableArray("tweens", null); + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule oc = ex.getCapsule(this); + oc.write(tweens, "tweens", null); + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/SequenceAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/SequenceAction.java new file mode 100644 index 000000000..08c59085a --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/SequenceAction.java @@ -0,0 +1,75 @@ +package com.jme3.anim.tween.action; + +import com.jme3.anim.tween.AbstractTween; +import com.jme3.anim.tween.Tween; + +public class SequenceAction extends Action { + + private int currentIndex = 0; + private double accumTime; + private double transitionTime = 0; + private float mainWeight = 1.0f; + private double transitionLength = 0.4f; + private TransitionTween transition = new TransitionTween(transitionLength); + + + public SequenceAction(Tween... tweens) { + super(tweens); + for (Tween tween : tweens) { + length += tween.getLength(); + } + } + + @Override + public float getWeightForTween(Tween tween) { + return weight * mainWeight; + } + + @Override + public boolean doInterpolate(double t) { + Tween currentTween = tweens[currentIndex]; + if (transition.getLength() > currentTween.getLength()) { + transition.setLength(currentTween.getLength()); + } + + transition.interpolate(t - transitionTime); + + boolean running = currentTween.interpolate(t - accumTime); + if (!running) { + accumTime += currentTween.getLength(); + currentIndex++; + transitionTime = accumTime; + transition.setLength(transitionLength); + } + + if (t >= length) { + reset(); + return false; + } + return true; + } + + public void reset() { + currentIndex = 0; + accumTime = 0; + transitionTime = 0; + mainWeight = 1; + } + + public void setTransitionLength(double transitionLength) { + this.transitionLength = transitionLength; + } + + private class TransitionTween extends AbstractTween { + + + public TransitionTween(double length) { + super(length); + } + + @Override + protected void doInterpolate(double t) { + mainWeight = (float) t; + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java index 799c4a7d6..c9e65b08f 100644 --- a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java +++ b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java @@ -61,10 +61,14 @@ public class AnimMigrationUtils { armature.setBindPose(); skeletonArmatureMap.put(skeleton, armature); + List tracks = new ArrayList<>(); + for (String animName : control.getAnimationNames()) { + tracks.clear(); Animation anim = control.getAnim(animName); AnimClip clip = new AnimClip(animName); Joint[] staticJoints = new Joint[joints.length]; + System.arraycopy(joints, 0, staticJoints, 0, joints.length); for (Track track : anim.getTracks()) { if (track instanceof BoneTrack) { @@ -72,17 +76,20 @@ public class AnimMigrationUtils { int index = boneTrack.getTargetBoneIndex(); Bone bone = skeleton.getBone(index); Joint joint = joints[index]; - JointTrack jointTrack = fromBoneTrack(boneTrack, bone, joint); - clip.addTrack(jointTrack); + TransformTrack jointTrack = fromBoneTrack(boneTrack, bone, joint); + tracks.add(jointTrack); //this joint is animated let's remove it from the static joints staticJoints[index] = null; } + //TODO spatial tracks , Effect tracks, Audio tracks } for (int i = 0; i < staticJoints.length; i++) { - padJointTracks(clip, staticJoints[i]); + padJointTracks(tracks, staticJoints[i]); } + clip.setTracks(tracks.toArray(new TransformTrack[tracks.size()])); + composer.addAnimClip(clip); } spatial.removeControl(control); @@ -95,7 +102,7 @@ public class AnimMigrationUtils { } } - public static void padJointTracks(AnimClip clip, Joint staticJoint) { + public static void padJointTracks(List tracks, Joint staticJoint) { Joint j = staticJoint; if (j != null) { // joint has no track , we create one with the default pose @@ -103,8 +110,8 @@ public class AnimMigrationUtils { Vector3f[] translations = new Vector3f[]{j.getLocalTranslation()}; Quaternion[] rotations = new Quaternion[]{j.getLocalRotation()}; Vector3f[] scales = new Vector3f[]{j.getLocalScale()}; - JointTrack track = new JointTrack(j, times, translations, rotations, scales); - clip.addTrack(track); + TransformTrack track = new TransformTrack(j, times, translations, rotations, scales); + tracks.add(track); } } @@ -144,7 +151,7 @@ public class AnimMigrationUtils { } } - public static JointTrack fromBoneTrack(BoneTrack boneTrack, Bone bone, Joint joint) { + public static TransformTrack fromBoneTrack(BoneTrack boneTrack, Bone bone, Joint joint) { float[] times = new float[boneTrack.getTimes().length]; int length = times.length; System.arraycopy(boneTrack.getTimes(), 0, times, 0, length); @@ -178,8 +185,8 @@ public class AnimMigrationUtils { scales[i] = newScale; } } - - return new JointTrack(joint, times, translations, rotations, scales); + TransformTrack t = new TransformTrack(joint, times, translations, rotations, scales); + return t; } private static Joint fromBone(Bone b) { diff --git a/jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java b/jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java new file mode 100644 index 000000000..28f560dfc --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java @@ -0,0 +1,10 @@ +package com.jme3.anim.util; + +import com.jme3.export.Savable; +import com.jme3.math.Transform; + +public interface HasLocalTransform extends Savable { + public void setLocalTransform(Transform transform); + + public Transform getLocalTransform(); +} diff --git a/jme3-core/src/main/java/com/jme3/anim/util/Weighted.java b/jme3-core/src/main/java/com/jme3/anim/util/Weighted.java new file mode 100644 index 000000000..8fb6d3255 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/util/Weighted.java @@ -0,0 +1,11 @@ +package com.jme3.anim.util; + +import com.jme3.anim.tween.action.Action; +import com.jme3.math.Transform; + +public interface Weighted { + + // public void setWeight(float weight); + + public void setParentAction(Action action); +} diff --git a/jme3-core/src/main/java/com/jme3/math/Transform.java b/jme3-core/src/main/java/com/jme3/math/Transform.java index 41e911bac..d9ac33ef0 100644 --- a/jme3-core/src/main/java/com/jme3/math/Transform.java +++ b/jme3-core/src/main/java/com/jme3/math/Transform.java @@ -176,7 +176,7 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable } /** - * Sets this matrix to the interpolation between the first matrix and the second by delta amount. + * Sets this transform to the interpolation between the first transform and the second by delta amount. * @param t1 The beginning transform. * @param t2 The ending transform. * @param delta An amount between 0 and 1 representing how far to interpolate from t1 to t2. diff --git a/jme3-core/src/main/java/com/jme3/scene/Spatial.java b/jme3-core/src/main/java/com/jme3/scene/Spatial.java index c36b5f347..c44db734b 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java +++ b/jme3-core/src/main/java/com/jme3/scene/Spatial.java @@ -31,6 +31,7 @@ */ package com.jme3.scene; +import com.jme3.anim.util.HasLocalTransform; import com.jme3.asset.AssetKey; import com.jme3.asset.CloneableSmartAsset; import com.jme3.bounding.BoundingVolume; @@ -67,7 +68,7 @@ import java.util.logging.Logger; * @author Joshua Slack * @version $Revision: 4075 $, $Data$ */ -public abstract class Spatial implements Savable, Cloneable, Collidable, CloneableSmartAsset, JmeCloneable { +public abstract class Spatial implements Savable, Cloneable, Collidable, CloneableSmartAsset, JmeCloneable, HasLocalTransform { private static final Logger logger = Logger.getLogger(Spatial.class.getName()); @@ -1792,4 +1793,4 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab } protected abstract void breadthFirstTraversal(SceneGraphVisitor visitor, Queue queue); -} \ No newline at end of file +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed100.frag b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed100.frag index 522971863..4fa7babec 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed100.frag +++ b/jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Misc/Dashed100.frag @@ -2,8 +2,8 @@ void main(){ startPos.xy = (startPos * 0.5 + 0.5).xy * resolution; float len = distance(gl_FragCoord.xy,startPos.xy); outColor = inColor; - float factor = int(len * 0.25); - if(mod(factor, 2) > 0.0){ + float factor = float(int(len * 0.25)); + if(mod(factor, 2.0) > 0.0){ discard; } diff --git a/jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java b/jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java index 6495c1de1..57dba2fb5 100644 --- a/jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java +++ b/jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java @@ -71,7 +71,7 @@ public class TestOgreConvert extends SimpleApplication { Node ogreModelReloaded = (Node) imp.load(bais, null, null); AnimComposer composer = ogreModelReloaded.getControl(AnimComposer.class); - composer.setCurrentAnimClip("Walk"); + composer.setCurrentAction("Walk"); rootNode.attachChild(ogreModelReloaded); } catch (IOException ex){ diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index ad85f3efe..06ec75f5f 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -116,9 +116,9 @@ public class TestGltfLoading extends SimpleApplication { //loadModel("Models/gltf/elephant/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/buffalo/scene.gltf", new Vector3f(0, -1, 0), 0.1f); //loadModel("Models/gltf/war/scene.gltf", new Vector3f(0, -1, 0), 0.1f); - loadModel("Models/gltf/ganjaarl/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + //loadModel("Models/gltf/ganjaarl/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/hero/scene.gltf", new Vector3f(0, -1, 0), 0.1f); - //loadModel("Models/gltf/mercy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); + loadModel("Models/gltf/mercy/scene.gltf", new Vector3f(0, -1, 0), 0.01f); //loadModel("Models/gltf/crab/scene.gltf", Vector3f.ZERO, 1); //loadModel("Models/gltf/manta/scene.gltf", Vector3f.ZERO, 0.2f); //loadModel("Models/gltf/bone/scene.gltf", Vector3f.ZERO, 0.1f); @@ -190,7 +190,7 @@ public class TestGltfLoading extends SimpleApplication { if (isPressed && composer != null) { String anim = anims.poll(); anims.add(anim); - composer.setCurrentAnimClip(anim); + composer.setCurrentAction(anim); } } }, "nextAnim"); @@ -262,7 +262,7 @@ public class TestGltfLoading extends SimpleApplication { } String anim = anims.poll(); anims.add(anim); - control.setCurrentAnimClip(anim); + control.setCurrentAction(anim); composer = control; } if (s instanceof Node) { diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index 4ed76312a..6f6f0a4c9 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -2,6 +2,7 @@ package jme3test.model.anim; import com.jme3.anim.AnimComposer; import com.jme3.anim.SkinningControl; +import com.jme3.anim.util.AnimMigrationUtils; import com.jme3.app.ChaseCameraAppState; import com.jme3.app.SimpleApplication; import com.jme3.input.KeyInput; @@ -24,8 +25,8 @@ public class TestAnimMigration extends SimpleApplication { ArmatureDebugAppState debugAppState; AnimComposer composer; - Queue anims = new LinkedList<>(); - boolean playAnim = true; + LinkedList anims = new LinkedList<>(); + boolean playAnim = false; public static void main(String... argv) { TestAnimMigration app = new TestAnimMigration(); @@ -40,12 +41,12 @@ public class TestAnimMigration extends SimpleApplication { rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); - //Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); - Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml").scale(0.2f).move(0, 1, 0); + Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + // Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml").scale(0.2f).move(0, 1, 0); //Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); //Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml").scale(0.02f); - // AnimMigrationUtils.migrate(model); + AnimMigrationUtils.migrate(model); rootNode.attachChild(model); @@ -87,7 +88,7 @@ public class TestAnimMigration extends SimpleApplication { if (playAnim) { String anim = anims.poll(); anims.add(anim); - composer.setCurrentAnimClip(anim); + composer.setCurrentAction(anim); System.err.println(anim); } else { composer.reset(); @@ -102,7 +103,7 @@ public class TestAnimMigration extends SimpleApplication { if (isPressed && composer != null) { String anim = anims.poll(); anims.add(anim); - composer.setCurrentAnimClip(anim); + composer.setCurrentAction(anim); System.err.println(anim); } } @@ -132,13 +133,26 @@ public class TestAnimMigration extends SimpleApplication { for (String name : composer.getAnimClipsNames()) { anims.add(name); } + composer.actionSequence("Sequence", + composer.tweenFromClip("Walk"), + composer.tweenFromClip("Run"), + composer.tweenFromClip("Jumping")); + +// composer.actionSequence("Sequence", +// composer.tweenFromClip("Walk"), +// composer.tweenFromClip("Dodge"), +// composer.tweenFromClip("push")); + + + anims.addFirst("Sequence"); + if (anims.isEmpty()) { return; } if (playAnim) { String anim = anims.poll(); anims.add(anim); - composer.setCurrentAnimClip(anim); + composer.setCurrentAction(anim); System.err.println(anim); } diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimSerialization.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimSerialization.java index 6b2196813..1d3551df2 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimSerialization.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimSerialization.java @@ -102,7 +102,7 @@ public class TestAnimSerialization extends SimpleApplication { if (playAnim) { String anim = anims.poll(); anims.add(anim); - composer.setCurrentAnimClip(anim); + composer.setCurrentAction(anim); System.err.println(anim); } else { composer.reset(); @@ -117,7 +117,7 @@ public class TestAnimSerialization extends SimpleApplication { if (isPressed && composer != null) { String anim = anims.poll(); anims.add(anim); - composer.setCurrentAnimClip(anim); + composer.setCurrentAction(anim); System.err.println(anim); } } @@ -144,7 +144,7 @@ public class TestAnimSerialization extends SimpleApplication { if (playAnim) { String anim = anims.poll(); anims.add(anim); - composer.setCurrentAnimClip(anim); + composer.setCurrentAction(anim); System.err.println(anim); } diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java index eefb08c75..f6b85674a 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java @@ -78,10 +78,10 @@ public class TestArmature extends SimpleApplication { new Vector3f(1, 1, 1), }; - JointTrack track1 = new JointTrack(j1, times, null, rotations, scales); - JointTrack track2 = new JointTrack(j2, times, null, rotations, null); - clip.addTrack(track1); - clip.addTrack(track2); + TransformTrack track1 = new TransformTrack(j1, times, null, rotations, scales); + TransformTrack track2 = new TransformTrack(j2, times, null, rotations, null); + + clip.setTracks(new TransformTrack[]{track1, track2}); //create the animComposer control final AnimComposer composer = new AnimComposer(); @@ -103,7 +103,7 @@ public class TestArmature extends SimpleApplication { node.addControl(composer); node.addControl(ac); - composer.setCurrentAnimClip("anim"); + composer.setCurrentAction("anim"); ArmatureDebugAppState debugAppState = new ArmatureDebugAppState(); debugAppState.addArmatureFrom(ac); @@ -134,7 +134,7 @@ public class TestArmature extends SimpleApplication { armature.resetToBindPose(); } else { - composer.setCurrentAnimClip("anim"); + composer.setCurrentAction("anim"); } } }, "bind"); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java b/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java index 512a777f3..748bb43f3 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestBaseAnimSerialization.java @@ -85,10 +85,10 @@ public class TestBaseAnimSerialization extends SimpleApplication { new Vector3f(1, 1, 1), }; - JointTrack track1 = new JointTrack(j1, times, null, rotations, scales); - JointTrack track2 = new JointTrack(j2, times, null, rotations, null); - clip.addTrack(track1); - clip.addTrack(track2); + TransformTrack track1 = new TransformTrack(j1, times, null, rotations, scales); + TransformTrack track2 = new TransformTrack(j2, times, null, rotations, null); + + clip.setTracks(new TransformTrack[]{track1, track2}); //create the animComposer control composer = new AnimComposer(); @@ -125,7 +125,7 @@ public class TestBaseAnimSerialization extends SimpleApplication { ac = newNode.getControl(SkinningControl.class); ac.setHardwareSkinningPreferred(false); armature = ac.getArmature(); - composer.setCurrentAnimClip("anim"); + composer.setCurrentAction("anim"); ArmatureDebugAppState debugAppState = new ArmatureDebugAppState(); debugAppState.addArmatureFrom(ac); @@ -156,7 +156,7 @@ public class TestBaseAnimSerialization extends SimpleApplication { armature.resetToBindPose(); } else { - composer.setCurrentAnimClip("anim"); + composer.setCurrentAction("anim"); } } }, "bind"); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java index 9e92cd35d..25544e8cc 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestHWSkinning.java @@ -48,9 +48,9 @@ import java.util.List; public class TestHWSkinning extends SimpleApplication implements ActionListener{ - private AnimComposer composer; + // private AnimComposer composer; private String[] animNames = {"Dodge", "Walk", "pull", "push"}; - private final static int SIZE = 50; + private final static int SIZE = 40; private boolean hwSkinningEnable = true; private List skControls = new ArrayList(); private BitmapText hwsText; @@ -80,9 +80,9 @@ public class TestHWSkinning extends SimpleApplication implements ActionListener{ Spatial model = (Spatial) assetManager.loadModel("Models/Oto/Oto.mesh.xml"); model.setLocalScale(0.1f); model.setLocalTranslation(i - SIZE / 2, 0, j - SIZE / 2); - composer = model.getControl(AnimComposer.class); + AnimComposer composer = model.getControl(AnimComposer.class); - composer.setCurrentAnimClip(animNames[(i + j) % 4]); + composer.setCurrentAction(animNames[(i + j) % 4]); SkinningControl skinningControl = model.getControl(SkinningControl.class); skinningControl.setHardwareSkinningPreferred(hwSkinningEnable); skControls.add(skinningControl); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestModelExportingCloning.java b/jme3-examples/src/main/java/jme3test/model/anim/TestModelExportingCloning.java index 80b6feb4c..63b5023d6 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestModelExportingCloning.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestModelExportingCloning.java @@ -60,19 +60,19 @@ public class TestModelExportingCloning extends SimpleApplication { Spatial originalModel = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); composer = originalModel.getControl(AnimComposer.class); - composer.setCurrentAnimClip("Walk"); + composer.setCurrentAction("Walk"); rootNode.attachChild(originalModel); Spatial clonedModel = originalModel.clone(); clonedModel.move(10, 0, 0); composer = clonedModel.getControl(AnimComposer.class); - composer.setCurrentAnimClip("push"); + composer.setCurrentAction("push"); rootNode.attachChild(clonedModel); Spatial exportedModel = BinaryExporter.saveAndLoad(assetManager, originalModel); exportedModel.move(20, 0, 0); composer = exportedModel.getControl(AnimComposer.class); - composer.setCurrentAnimClip("pull"); + composer.setCurrentAction("pull"); rootNode.attachChild(exportedModel); } } 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 a2bc44aec..d77675e27 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 @@ -793,6 +793,7 @@ public class GltfLoader implements AssetLoader { List spatials = new ArrayList<>(); AnimClip anim = new AnimClip(name); + List ttracks = new ArrayList<>(); int skinIndex = -1; List usedJoints = new ArrayList<>(); @@ -806,8 +807,8 @@ public class GltfLoader implements AssetLoader { if (node instanceof Spatial) { Spatial s = (Spatial) node; spatials.add(s); - SpatialTrack track = new SpatialTrack(s, trackData.times, trackData.translations, trackData.rotations, trackData.scales); - anim.addTrack(track); + TransformTrack track = new TransformTrack(s, trackData.times, trackData.translations, trackData.rotations, trackData.scales); + ttracks.add(track); } else if (node instanceof JointWrapper) { JointWrapper jw = (JointWrapper) node; usedJoints.add(jw.joint); @@ -822,8 +823,8 @@ public class GltfLoader implements AssetLoader { } } - JointTrack track = new JointTrack(jw.joint, trackData.times, trackData.translations, trackData.rotations, trackData.scales); - anim.addTrack(track); + TransformTrack track = new TransformTrack(jw.joint, trackData.times, trackData.translations, trackData.rotations, trackData.scales); + ttracks.add(track); } } @@ -834,19 +835,21 @@ public class GltfLoader implements AssetLoader { if (skinIndex != -1) { SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class); for (Joint joint : skin.joints) { - if (!usedJoints.contains(joint)) {// && !equalBindAndLocalTransforms(joint) + if (!usedJoints.contains(joint)) { //create a track float[] times = new float[]{0}; Vector3f[] translations = new Vector3f[]{joint.getLocalTranslation()}; Quaternion[] rotations = new Quaternion[]{joint.getLocalRotation()}; Vector3f[] scales = new Vector3f[]{joint.getLocalScale()}; - JointTrack track = new JointTrack(joint, times, translations, rotations, scales); - anim.addTrack(track); + TransformTrack track = new TransformTrack(joint, times, translations, rotations, scales); + ttracks.add(track); } } } + anim.setTracks(ttracks.toArray(new TransformTrack[ttracks.size()])); + anim = customContentManager.readExtensionAndExtras("animations", animation, anim); if (skinIndex != -1) { diff --git a/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java b/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java index b1350c93c..88ab22853 100644 --- a/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java +++ b/jme3-plugins/src/ogre/java/com/jme3/scene/plugins/ogre/SkeletonLoader.java @@ -54,16 +54,16 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { private Stack elementStack = new Stack(); private HashMap indexToJoint = new HashMap<>(); private HashMap nameToJoint = new HashMap<>(); - private JointTrack track; - private ArrayList tracks = new ArrayList<>(); + private TransformTrack track; + private ArrayList tracks = new ArrayList<>(); private AnimClip animClip; private ArrayList animClips; private Joint joint; private Armature armature; - private ArrayList times = new ArrayList(); - private ArrayList translations = new ArrayList(); - private ArrayList rotations = new ArrayList(); - private ArrayList scales = new ArrayList(); + private ArrayList times = new ArrayList<>(); + private ArrayList translations = new ArrayList<>(); + private ArrayList rotations = new ArrayList<>(); + private ArrayList scales = new ArrayList<>(); private float time = -1; private Vector3f position; private Quaternion rotation; @@ -92,7 +92,7 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { assert elementStack.peek().equals("tracks"); String jointName = SAXUtil.parseString(attribs.getValue("bone")); joint = nameToJoint.get(jointName); - track = new JointTrack(); + track = new TransformTrack(); track.setTarget(joint); } else if (qName.equals("boneparent")) { assert elementStack.peek().equals("bonehierarchy"); @@ -163,10 +163,6 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { armature = new Armature(joints); armature.setBindPose(); } else if (qName.equals("animation")) { - //nameToJoint contains the joints with no track - for (Joint j : unusedJoints) { - AnimMigrationUtils.padJointTracks(animClip, j); - } animClips.add(animClip); animClip = null; } else if (qName.equals("track")) { @@ -176,7 +172,11 @@ public class SkeletonLoader extends DefaultHandler implements AssetLoader { track = null; } } else if (qName.equals("tracks")) { - JointTrack[] trackList = tracks.toArray(new JointTrack[tracks.size()]); + //nameToJoint contains the joints with no track + for (Joint j : unusedJoints) { + AnimMigrationUtils.padJointTracks(tracks, j); + } + TransformTrack[] trackList = tracks.toArray(new TransformTrack[tracks.size()]); animClip.setTracks(trackList); tracks.clear(); } else if (qName.equals("keyframe")) { From 2fc3bf5cfd26f6170875ce1c84aea0940aeac7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81my=20Bouquet?= Date: Sun, 31 Dec 2017 10:23:07 +0100 Subject: [PATCH 20/54] Added a BlendAction that allows to blend animations --- .../main/java/com/jme3/anim/AnimComposer.java | 10 +- .../com/jme3/anim/tween/AbstractTween.java | 13 - .../com/jme3/anim/tween/AnimClipTween.java | 20 +- .../main/java/com/jme3/anim/tween/Tween.java | 2 +- .../main/java/com/jme3/anim/tween/Tweens.java | 603 ++++++++++++++++++ .../com/jme3/anim/tween/action/Action.java | 17 +- .../jme3/anim/tween/action/BlendAction.java | 80 +++ .../jme3/anim/tween/action/BlendSpace.java | 12 + .../anim/tween/action/LinearBlendSpace.java | 50 ++ .../java/com/jme3/anim/util/Primitives.java | 56 ++ .../model/anim/TestAnimMigration.java | 32 + 11 files changed, 845 insertions(+), 50 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java create mode 100644 jme3-core/src/main/java/com/jme3/anim/util/Primitives.java diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index 1515136ef..1bdb41176 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -3,6 +3,8 @@ package com.jme3.anim; import com.jme3.anim.tween.AnimClipTween; import com.jme3.anim.tween.Tween; import com.jme3.anim.tween.action.Action; +import com.jme3.anim.tween.action.BlendAction; +import com.jme3.anim.tween.action.BlendSpace; import com.jme3.anim.tween.action.SequenceAction; import com.jme3.export.*; import com.jme3.renderer.RenderManager; @@ -92,8 +94,10 @@ public class AnimComposer extends AbstractControl { return action; } - public Action actionBlended(String name, Tween... tweens) { - return null; + public BlendAction actionBlended(String name, BlendSpace blendSpace, Tween... tweens) { + BlendAction action = new BlendAction(blendSpace, tweens); + actions.put(name, action); + return action; } public void reset() { @@ -155,7 +159,6 @@ public class AnimComposer extends AbstractControl { super.read(im); InputCapsule ic = im.getCapsule(this); animClipMap = (Map) ic.readStringSavableMap("animClipMap", new HashMap()); - actions = (Map) ic.readStringSavableMap("actions", new HashMap()); } @Override @@ -163,6 +166,5 @@ public class AnimComposer extends AbstractControl { super.write(ex); OutputCapsule oc = ex.getCapsule(this); oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap()); - oc.writeStringSavableMap(actions, "actions", new HashMap()); } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java b/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java index aa3407268..272ec8dae 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java @@ -94,17 +94,4 @@ public abstract class AbstractTween implements Tween { } protected abstract void doInterpolate(double t); - - @Override - public void write(JmeExporter ex) throws IOException { - OutputCapsule oc = ex.getCapsule(this); - oc.write(length, "length", 0); - } - - @Override - public void read(JmeImporter im) throws IOException { - InputCapsule ic = im.getCapsule(this); - length = ic.readDouble("length", 0); - } } - \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java b/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java index 384abe31e..a36936244 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java @@ -43,6 +43,10 @@ public class AnimClipTween implements Tween, Weighted, JmeCloneable { if (parentAction != null) { weight = parentAction.getWeightForTween(this); } + if (weight == 0) { + //weight is 0 let's not interpolate + return t < clip.getLength(); + } TransformTrack[] tracks = clip.getTracks(); for (TransformTrack track : tracks) { HasLocalTransform target = track.getTarget(); @@ -60,17 +64,6 @@ public class AnimClipTween implements Tween, Weighted, JmeCloneable { return t < clip.getLength(); } - @Override - public void write(JmeExporter ex) throws IOException { - OutputCapsule oc = ex.getCapsule(this); - oc.write(clip, "clip", null); - } - - @Override - public void read(JmeImporter im) throws IOException { - InputCapsule ic = im.getCapsule(this); - clip = (AnimClip) ic.readSavable("clip", null); - } @Override public Object jmeClone() { @@ -87,11 +80,6 @@ public class AnimClipTween implements Tween, Weighted, JmeCloneable { clip = cloner.clone(clip); } -// @Override -// public void setWeight(float weight) { -// this.weight = weight; -// } - @Override public void setParentAction(Action action) { this.parentAction = action; diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/Tween.java b/jme3-core/src/main/java/com/jme3/anim/tween/Tween.java index 450eb5f44..9ed4752c8 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/Tween.java +++ b/jme3-core/src/main/java/com/jme3/anim/tween/Tween.java @@ -46,7 +46,7 @@ import com.jme3.export.Savable; * * @author Paul Speed */ -public interface Tween extends Savable, Cloneable { +public interface Tween extends Cloneable { /** * Returns the length of the tween. If 't' represents time in diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java b/jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java new file mode 100644 index 000000000..b369cadce --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java @@ -0,0 +1,603 @@ +/* + * $Id$ + * + * Copyright (c) 2015, Simsilica, LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. 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. + * + * 3. Neither the name of the copyright holder 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 HOLDER 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.anim.tween; + +import com.jme3.anim.util.Primitives; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Static utility methods for creating common generic Tween objects. + * + * @author Paul Speed + */ +public class Tweens { + + static Logger log = Logger.getLogger(Tweens.class.getName()); + + private static final CurveFunction SMOOTH = new SmoothStep(); + private static final CurveFunction SINE = new Sine(); + + /** + * Creates a tween that will interpolate over an entire sequence + * of tweens in order. + */ + public static Tween sequence(Tween... delegates) { + return new Sequence(delegates); + } + + /** + * Creates a tween that will interpolate over an entire list + * of tweens in parallel, ie: all tweens will be run at the same + * time. + */ + public static Tween parallel(Tween... delegates) { + return new Parallel(delegates); + } + + /** + * Creates a tween that will perform a no-op until the length + * has expired. + */ + public static Tween delay(double length) { + return new Delay(length); + } + + /** + * Creates a tween that scales the specified delegate tween or tweens + * to the desired length. If more than one tween is specified then they + * are wrapped in a sequence using the sequence() method. + */ + public static Tween stretch(double desiredLength, Tween... delegates) { + if (delegates.length == 1) { + return new Stretch(delegates[0], desiredLength); + } + return new Stretch(sequence(delegates), desiredLength); + } + + /** + * Creates a tween that uses a sine function to smooth step the time value + * for the specified delegate tween or tweens. These 'curved' wrappers + * can be used to smooth the interpolation of another tween. + */ + public static Tween sineStep(Tween... delegates) { + if (delegates.length == 1) { + return new Curve(delegates[0], SINE); + } + return new Curve(sequence(delegates), SINE); + } + + /** + * Creates a tween that uses a hermite function to smooth step the time value + * for the specified delegate tween or tweens. This is similar to GLSL's + * smoothstep(). These 'curved' wrappers can be used to smooth the interpolation + * of another tween. + */ + public static Tween smoothStep(Tween... delegates) { + if (delegates.length == 1) { + return new Curve(delegates[0], SMOOTH); + } + return new Curve(sequence(delegates), SMOOTH); + } + + /** + * Creates a Tween that will call the specified method and optional arguments + * whenever supplied a time value greater than or equal to 0. This creates + * an "instant" tween of length 0. + */ + public static Tween callMethod(Object target, String method, Object... args) { + return new CallMethod(target, method, args); + } + + /** + * Creates a Tween that will call the specified method and optional arguments, + * including the time value scaled between 0 and 1. The method must take + * a float or double value as its first or last argument, in addition to whatever + * optional arguments are specified. + *