package com.jme3.scene.plugins.blender.meshes;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.TreeMap;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
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.util.BufferUtils;
/**
* A class that aggregates the mesh data to prepare proper buffers. The buffers refer only to ONE material.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class MeshBuffers {
private static final int MAXIMUM_WEIGHTS_PER_VERTEX = 4;
/** The material index. */
private final int materialIndex;
/** The vertices. */
private List<Vector3f> verts = new ArrayList<Vector3f>();
/** The normals. */
private List<Vector3f> normals = new ArrayList<Vector3f>();
/** The UV coordinate sets. */
private Map<String, List<Vector2f>> uvCoords = new HashMap<String, List<Vector2f>>();
/** The vertex colors. */
private List<byte[]> vertColors = new ArrayList<byte[]>();
/** The indexes. */
private List<Integer> indexes = new ArrayList<Integer>();
/** The maximum weights count assigned to a single vertex. Used during weights normalization. */
private int maximumWeightsPerVertex;
/** A list of mapping between weights and indexes. Each entry for the proper vertex. */
private List<TreeMap<Float, Integer>> boneWeightAndIndexes = new ArrayList<TreeMap<Float, Integer>>();
/**
* Constructor stores only the material index value.
* @param materialIndex
* the material index
*/
public MeshBuffers(int materialIndex) {
this.materialIndex = materialIndex;
}
/**
* @return the material index
*/
public int getMaterialIndex() {
return materialIndex;
}
/**
* @return indexes buffer
*/
public Buffer getIndexBuffer() {
if (indexes.size() <= Short.MAX_VALUE) {
short[] indices = new short[indexes.size()];
for (int i = 0; i < indexes.size(); ++i) {
indices[i] = indexes.get(i).shortValue();
}
return BufferUtils.createShortBuffer(indices);
} else {
int[] indices = new int[indexes.size()];
for (int i = 0; i < indexes.size(); ++i) {
indices[i] = indexes.get(i).intValue();
}
return BufferUtils.createIntBuffer(indices);
}
}
/**
* @return positions buffer
*/
public VertexBuffer getPositionsBuffer() {
VertexBuffer positionBuffer = new VertexBuffer(Type.Position);
Vector3f[] data = verts.toArray(new Vector3f[verts.size()]);
positionBuffer.setupData(Usage.Static, 3, Format.Float, BufferUtils.createFloatBuffer(data));
return positionBuffer;
}
/**
* @return normals buffer
*/
public VertexBuffer getNormalsBuffer() {
VertexBuffer positionBuffer = new VertexBuffer(Type.Normal);
Vector3f[] data = normals.toArray(new Vector3f[normals.size()]);
positionBuffer.setupData(Usage.Static, 3, Format.Float, BufferUtils.createFloatBuffer(data));
return positionBuffer;
}
/**
* @return bone buffers
*/
public BoneBuffersData getBoneBuffers() {
BoneBuffersData result = null;
if (maximumWeightsPerVertex > 0) {
this.normalizeBoneBuffers(MAXIMUM_WEIGHTS_PER_VERTEX);
maximumWeightsPerVertex = MAXIMUM_WEIGHTS_PER_VERTEX;
FloatBuffer weightsFloatData = BufferUtils.createFloatBuffer(boneWeightAndIndexes.size() * MAXIMUM_WEIGHTS_PER_VERTEX);
ByteBuffer indicesData = BufferUtils.createByteBuffer(boneWeightAndIndexes.size() * MAXIMUM_WEIGHTS_PER_VERTEX);
int index = 0;
for (Map<Float, Integer> boneBuffersData : boneWeightAndIndexes) {
if (boneBuffersData.size() > 0) {
int count = 0;
for (Entry<Float, Integer> entry : boneBuffersData.entrySet()) {
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + count, entry.getKey());
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + count, entry.getValue().byteValue());
++count;
}
} else {
// if no bone is assigned to this vertex then attach it to the 0-indexed root bone
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, 1.0f);
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, (byte) 0);
}
++index;
}
VertexBuffer verticesWeights = new VertexBuffer(Type.BoneWeight);
verticesWeights.setupData(Usage.CpuOnly, maximumWeightsPerVertex, Format.Float, weightsFloatData);
VertexBuffer verticesWeightsIndices = new VertexBuffer(Type.BoneIndex);
verticesWeightsIndices.setupData(Usage.CpuOnly, maximumWeightsPerVertex, Format.UnsignedByte, indicesData);
result = new BoneBuffersData(maximumWeightsPerVertex, verticesWeights, verticesWeightsIndices);
}
return result;
}
/**
* @return UV coordinates sets
*/
public Map<String, List<Vector2f>> getUvCoords() {
return uvCoords;
}
/**
* @return <b>true</b> if vertex colors are used and <b>false</b> otherwise
*/
public boolean areVertexColorsUsed() {
return vertColors.size() > 0;
}
/**
* @return vertex colors buffer
*/
public ByteBuffer getVertexColorsBuffer() {
ByteBuffer result = null;
if (vertColors.size() > 0) {
result = BufferUtils.createByteBuffer(4 * vertColors.size());
for (byte[] v : vertColors) {
if (v != null) {
result.put(v[0]).put(v[1]).put(v[2]).put(v[3]);
} else {
result.put((byte) 0).put((byte) 0).put((byte) 0).put((byte) 0);
}
}
result.flip();
}
return result;
}
/**
* @return <b>true</b> if indexes can be shorts' and <b>false</b> if they need to be ints'
*/
public boolean isShortIndexBuffer() {
return indexes.size() <= Short.MAX_VALUE;
}
/**
* Appends a vertex and normal to the buffers.
* @param vert
* vertex
* @param normal
* normal vector
*/
public void append(Vector3f vert, Vector3f normal) {
int index = this.indexOf(vert, normal, null);
if (index >= 0) {
indexes.add(index);
} else {
indexes.add(verts.size());
verts.add(vert);
normals.add(normal);
}
}
/**
* Appends the face data to the buffers.
* @param smooth
* tells if the face is smooth or flat
* @param verts
* the vertices
* @param normals
* the normals
* @param uvCoords
* the UV coordinates
* @param vertColors
* the vertex colors
* @param vertexGroups
* the vertex groups
*/
public void append(boolean smooth, Vector3f[] verts, Vector3f[] normals, Map<String, List<Vector2f>> uvCoords, byte[][] vertColors, List<Map<Float, Integer>> vertexGroups) {
if (verts.length != normals.length) {
throw new IllegalArgumentException("The amount of verts and normals MUST be equal!");
}
if (vertColors != null && vertColors.length != verts.length) {
throw new IllegalArgumentException("The amount of vertex colors and vertices MUST be equal!");
}
if (vertexGroups.size() != 0 && vertexGroups.size() != verts.length) {
throw new IllegalArgumentException("The amount of (if given) vertex groups and vertices MUST be equal!");
}
if (!smooth) {
// make the normals perpendicular to the face
normals[0] = normals[1] = normals[2] = FastMath.computeNormal(verts[0], verts[1], verts[2]);
}
for (int i = 0; i < verts.length; ++i) {
int index = -1;
Map<String, Vector2f> uvCoordsForVertex = this.getUVsForVertex(i, uvCoords);
if (smooth && (index = this.indexOf(verts[i], normals[i], uvCoordsForVertex)) >= 0) {
indexes.add(index);
} else {
indexes.add(this.verts.size());
this.verts.add(verts[i]);
this.normals.add(normals[i]);
this.vertColors.add(vertColors[i]);
if (uvCoords != null && uvCoords.size() > 0) {
for (Entry<String, List<Vector2f>> entry : uvCoords.entrySet()) {
if (this.uvCoords.containsKey(entry.getKey())) {
this.uvCoords.get(entry.getKey()).add(entry.getValue().get(i));
} else {
List<Vector2f> uvs = new ArrayList<Vector2f>();
uvs.add(entry.getValue().get(i));
this.uvCoords.put(entry.getKey(), uvs);
}
}
}
if (vertexGroups != null && vertexGroups.size() > 0) {
Map<Float, Integer> group = vertexGroups.get(i);
maximumWeightsPerVertex = Math.max(maximumWeightsPerVertex, group.size());
boneWeightAndIndexes.add(new TreeMap<Float, Integer>(group));
}
}
}
}
/**
* Returns UV coordinates assigned for the vertex with the proper index.
* @param vertexIndex
* the index of the vertex
* @param uvs
* all UV coordinates we search in
* @return a set of UV coordinates assigned to the given vertex
*/
private Map<String, Vector2f> getUVsForVertex(int vertexIndex, Map<String, List<Vector2f>> uvs) {
if (uvs == null || uvs.size() == 0) {
return null;
}
Map<String, Vector2f> result = new HashMap<String, Vector2f>(uvs.size());
for (Entry<String, List<Vector2f>> entry : uvs.entrySet()) {
result.put(entry.getKey(), entry.getValue().get(vertexIndex));
}
return result;
}
/**
* The method returns an index of a vertex described by the given data.
* The method tries to find a vertex that mathes the given data. If it does it means
* that such vertex is already used.
* @param vert
* the vertex position coordinates
* @param normal
* the vertex's normal vector
* @param uvCoords
* the UV coords of the vertex
* @return index of the found vertex of -1
*/
private int indexOf(Vector3f vert, Vector3f normal, Map<String, Vector2f> uvCoords) {
for (int i = 0; i < verts.size(); ++i) {
if (verts.get(i).equals(vert) && normals.get(i).equals(normal)) {
if (uvCoords != null && uvCoords.size() > 0) {
for (Entry<String, Vector2f> entry : uvCoords.entrySet()) {
List<Vector2f> uvs = this.uvCoords.get(entry.getKey());
if (uvs == null) {
return -1;
}
if (!uvs.get(i).equals(entry.getValue())) {
return -1;
}
}
}
return i;
}
}
return -1;
}
/**
* The method normalizes the weights and bone indexes data.
* First it truncates the amount to MAXIMUM_WEIGHTS_PER_VERTEX because this is how many weights JME can handle.
* Next it normalizes the weights so that the sum of all verts is 1.
* @param maximumSize
* the maximum size that the data will be truncated to (usually: MAXIMUM_WEIGHTS_PER_VERTEX)
*/
private void normalizeBoneBuffers(int maximumSize) {
for (TreeMap<Float, Integer> group : boneWeightAndIndexes) {
if (group.size() > maximumSize) {
NavigableMap<Float, Integer> descendingWeights = group.descendingMap();
while (descendingWeights.size() > maximumSize) {
descendingWeights.pollLastEntry();
}
}
// normalizing the weights so that the sum of the values is equal to '1'
TreeMap<Float, Integer> normalizedGroup = new TreeMap<Float, Integer>();
float sum = 0;
for (Entry<Float, Integer> entry : group.entrySet()) {
sum += entry.getKey();
}
if (sum != 0 && sum != 1) {
for (Entry<Float, Integer> entry : group.entrySet()) {
normalizedGroup.put(entry.getKey() / sum, entry.getValue());
}
group.clear();
group.putAll(normalizedGroup);
}
}
}
/**
* A class that gathers the data for mesh bone buffers.
* Added to increase code readability.
*
* @author Marcin Roguski (Kaelthas)
*/
public static class BoneBuffersData {
public final int maximumWeightsPerVertex;
public final VertexBuffer verticesWeights;
public final VertexBuffer verticesWeightsIndices;
public BoneBuffersData(int maximumWeightsPerVertex, VertexBuffer verticesWeights, VertexBuffer verticesWeightsIndices) {
this.maximumWeightsPerVertex = maximumWeightsPerVertex;
this.verticesWeights = verticesWeights;
this.verticesWeightsIndices = verticesWeightsIndices;
}
}
}