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 +}