com.jme3.scene.plugins.blender.textures.TriangulatedTexture Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jme3-blender Show documentation
Show all versions of jme3-blender Show documentation
jMonkeyEngine is a 3D game engine for adventurous Java developers
The newest version!
package com.jme3.scene.plugins.blender.textures;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import jme3tools.converters.ImageToAwt;
import com.jme3.bounding.BoundingBox;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.textures.blending.TextureBlender;
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;
/**
* This texture holds a set of images for each face in the specified mesh. It
* helps to flatten 3D texture, merge 3D and 2D textures and merge 2D textures
* with different UV coordinates.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class TriangulatedTexture extends Texture2D {
/** The result image format. */
private Format format;
/** The collection of images for each face. */
private Collection faceTextures;
/**
* The maximum texture size (width/height). This is taken from the blender
* key.
*/
private int maxTextureSize;
/** A variable that can prevent removing identical textures. */
private boolean keepIdenticalTextures = false;
/** The result texture. */
private Texture2D resultTexture;
/** The result texture's UV coordinates. */
private List resultUVS;
/**
* This method triangulates the given flat texture. The given texture is not
* changed.
*
* @param texture2d
* the texture to be triangulated
* @param uvs
* the UV coordinates for each face
*/
public TriangulatedTexture(Texture2D texture2d, List uvs, BlenderContext blenderContext) {
maxTextureSize = blenderContext.getBlenderKey().getMaxTextureSize();
faceTextures = new TreeSet(new Comparator() {
public int compare(TriangleTextureElement o1, TriangleTextureElement o2) {
return o1.faceIndex - o2.faceIndex;
}
});
int facesCount = uvs.size() / 3;
for (int i = 0; i < facesCount; ++i) {
faceTextures.add(new TriangleTextureElement(i, texture2d.getImage(), uvs, true, blenderContext));
}
format = texture2d.getImage().getFormat();
}
/**
* Constructor that simply stores precalculated images.
*
* @param faceTextures
* a collection of images for the mesh's faces
* @param blenderContext
* the blender context
*/
public TriangulatedTexture(Collection faceTextures, BlenderContext blenderContext) {
maxTextureSize = blenderContext.getBlenderKey().getMaxTextureSize();
this.faceTextures = faceTextures;
for (TriangleTextureElement faceTextureElement : faceTextures) {
if (format == null) {
format = faceTextureElement.image.getFormat();
} else if (format != faceTextureElement.image.getFormat()) {
throw new IllegalArgumentException("Face texture element images MUST have the same image format!");
}
}
}
/**
* This method blends the each image using the given blender and taking base
* texture into consideration.
*
* @param textureBlender
* the texture blender that holds the blending definition
* @param baseTexture
* the texture that is 'below' the current texture (can be null)
* @param blenderContext
* the blender context
*/
public void blend(TextureBlender textureBlender, TriangulatedTexture baseTexture, BlenderContext blenderContext) {
Format newFormat = null;
for (TriangleTextureElement triangleTextureElement : faceTextures) {
Image baseImage = baseTexture == null ? null : baseTexture.getFaceTextureElement(triangleTextureElement.faceIndex).image;
triangleTextureElement.image = textureBlender.blend(triangleTextureElement.image, baseImage, blenderContext);
if (newFormat == null) {
newFormat = triangleTextureElement.image.getFormat();
} else if (newFormat != triangleTextureElement.image.getFormat()) {
throw new IllegalArgumentException("Face texture element images MUST have the same image format!");
}
}
format = newFormat;
}
/**
* This method alters the images to fit them into UV coordinates of the
* given target texture.
*
* @param targetTexture
* the texture to whose UV coordinates we fit current images
* @param blenderContext
* the blender context
*/
public void castToUVS(TriangulatedTexture targetTexture, BlenderContext blenderContext) {
int[] sourceSize = new int[2], targetSize = new int[2];
ImageLoader imageLoader = new ImageLoader();
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
for (TriangleTextureElement entry : faceTextures) {
TriangleTextureElement targetFaceTextureElement = targetTexture.getFaceTextureElement(entry.faceIndex);
Vector2f[] dest = targetFaceTextureElement.uv;
// get the sizes of the source and target images
sourceSize[0] = entry.image.getWidth();
sourceSize[1] = entry.image.getHeight();
targetSize[0] = targetFaceTextureElement.image.getWidth();
targetSize[1] = targetFaceTextureElement.image.getHeight();
// create triangle transformation
AffineTransform affineTransform = textureHelper.createAffineTransform(entry.uv, dest, sourceSize, targetSize);
// compute the result texture
BufferedImage sourceImage = ImageToAwt.convert(entry.image, false, true, 0);
BufferedImage targetImage = new BufferedImage(targetSize[0], targetSize[1], sourceImage.getType());
Graphics2D g = targetImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(sourceImage, affineTransform, null);
g.dispose();
Image output = imageLoader.load(targetImage, false);
entry.image = output;
entry.uv[0].set(dest[0]);
entry.uv[1].set(dest[1]);
entry.uv[2].set(dest[2]);
}
}
/**
* This method returns the flat texture. It is calculated if required or if
* it was not created before. Images that are identical are discarded to
* reduce the texture size.
*
* @param rebuild
* a variable that forces texture recomputation (even if it was
* computed vefore)
* @return flat result texture (all images merged into one)
*/
public Texture2D getResultTexture(boolean rebuild) {
if (resultTexture == null || rebuild) {
// sorting the parts by their height (from highest to the lowest)
List list = new ArrayList(faceTextures);
Collections.sort(list, new Comparator() {
public int compare(TriangleTextureElement o1, TriangleTextureElement o2) {
return o2.image.getHeight() - o1.image.getHeight();
}
});
// arraging the images on the resulting image (calculating the result image width and height)
Set duplicatedFaceIndexes = new HashSet();
int resultImageHeight = list.get(0).image.getHeight();
int resultImageWidth = 0;
int currentXPos = 0, currentYPos = 0;
Map imageLayoutData = new HashMap(list.size());
while (list.size() > 0) {
TriangleTextureElement currentElement = list.remove(0);
if (currentXPos + currentElement.image.getWidth() > maxTextureSize) {
currentXPos = 0;
currentYPos = resultImageHeight;
resultImageHeight += currentElement.image.getHeight();
}
Integer[] currentPositions = new Integer[] { currentXPos, currentYPos };
imageLayoutData.put(currentElement, currentPositions);
if (keepIdenticalTextures) {// removing identical images
for (int i = 0; i < list.size(); ++i) {
if (currentElement.image.equals(list.get(i).image)) {
duplicatedFaceIndexes.add(list.get(i).faceIndex);
imageLayoutData.put(list.remove(i--), currentPositions);
}
}
}
currentXPos += currentElement.image.getWidth();
resultImageWidth = Math.max(resultImageWidth, currentXPos);
// currentYPos += currentElement.image.getHeight();
// TODO: implement that to compact the result image
// try to add smaller images below the current one
// int remainingHeight = resultImageHeight -
// currentElement.image.getHeight();
// while(remainingHeight > 0) {
// for(int i=list.size() - 1;i>=0;--i) {
//
// }
// }
}
// computing the result UV coordinates
resultUVS = new ArrayList(imageLayoutData.size() * 3);
for (int i = 0; i < imageLayoutData.size() * 3; ++i) {
resultUVS.add(null);
}
Vector2f[] uvs = new Vector2f[3];
for (Entry entry : imageLayoutData.entrySet()) {
Integer[] position = entry.getValue();
entry.getKey().computeFinalUVCoordinates(resultImageWidth, resultImageHeight, position[0], position[1], uvs);
resultUVS.set(entry.getKey().faceIndex * 3, uvs[0]);
resultUVS.set(entry.getKey().faceIndex * 3 + 1, uvs[1]);
resultUVS.set(entry.getKey().faceIndex * 3 + 2, uvs[2]);
}
Image resultImage = new Image(format, resultImageWidth, resultImageHeight, BufferUtils.createByteBuffer(resultImageWidth * resultImageHeight * (format.getBitsPerPixel() >> 3)), ColorSpace.Linear);
resultTexture = new Texture2D(resultImage);
for (Entry entry : imageLayoutData.entrySet()) {
if (!duplicatedFaceIndexes.contains(entry.getKey().faceIndex)) {
this.draw(resultImage, entry.getKey().image, entry.getValue()[0], entry.getValue()[1]);
}
}
// setting additional data
resultTexture.setWrap(WrapAxis.S, this.getWrap(WrapAxis.S));
resultTexture.setWrap(WrapAxis.T, this.getWrap(WrapAxis.T));
resultTexture.setMagFilter(this.getMagFilter());
resultTexture.setMinFilter(this.getMinFilter());
}
return resultTexture;
}
/**
* @return the result flat texture
*/
public Texture2D getResultTexture() {
return this.getResultTexture(false);
}
/**
* @return the result texture's UV coordinates
*/
public List getResultUVS() {
this.getResultTexture();// this is called here to make sure that the result UVS are computed
return resultUVS;
}
/**
* This method returns a single image element for the given face index.
*
* @param faceIndex
* the face index
* @return image element for the required face index
* @throws IllegalStateException
* this exception is thrown if the current image set does not
* contain an image for the given face index
*/
public TriangleTextureElement getFaceTextureElement(int faceIndex) {
for (TriangleTextureElement textureElement : faceTextures) {
if (textureElement.faceIndex == faceIndex) {
return textureElement;
}
}
throw new IllegalStateException("No face texture element found for index: " + faceIndex);
}
/**
* @return the amount of texture faces
*/
public int getFaceTextureCount() {
return faceTextures.size();
}
/**
* Tells the object wheather to keep or reduce identical face textures.
*
* @param keepIdenticalTextures
* keeps or discards identical textures
*/
public void setKeepIdenticalTextures(boolean keepIdenticalTextures) {
this.keepIdenticalTextures = keepIdenticalTextures;
}
/**
* This method draws the source image on the target image starting with the
* specified positions.
*
* @param target
* the target image
* @param source
* the source image
* @param targetXPos
* start X position on the target image
* @param targetYPos
* start Y position on the target image
*/
private void draw(Image target, Image source, int targetXPos, int targetYPos) {
PixelInputOutput sourceIO = PixelIOFactory.getPixelIO(source.getFormat());
PixelInputOutput targetIO = PixelIOFactory.getPixelIO(target.getFormat());
TexturePixel pixel = new TexturePixel();
for (int x = 0; x < source.getWidth(); ++x) {
for (int y = 0; y < source.getHeight(); ++y) {
sourceIO.read(source, 0, pixel, x, y);
targetIO.write(target, 0, pixel, targetXPos + x, targetYPos + y);
}
}
}
/**
* A class that represents an image for a single face of the mesh.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */static class TriangleTextureElement {
/** The image for the face. */
public Image image;
/** The UV coordinates for the image. */
public final Vector2f[] uv;
/** The index of the face this image refers to. */
public final int faceIndex;
/**
* Constructor that creates the image element from the given texture and
* UV coordinates (it cuts out the smallest rectasngle possible from the
* given image that will hold the triangle defined by the given UV
* coordinates). After the image is cut out the UV coordinates are
* recalculated to be fit for the new image.
*
* @param faceIndex
* the index of mesh's face this image refers to
* @param sourceImage
* the source image
* @param uvCoordinates
* the UV coordinates that define the image
*/
public TriangleTextureElement(int faceIndex, Image sourceImage, List uvCoordinates, boolean wholeUVList, BlenderContext blenderContext) {
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
this.faceIndex = faceIndex;
uv = wholeUVList ? new Vector2f[] { uvCoordinates.get(faceIndex * 3).clone(), uvCoordinates.get(faceIndex * 3 + 1).clone(), uvCoordinates.get(faceIndex * 3 + 2).clone() } : new Vector2f[] { uvCoordinates.get(0).clone(), uvCoordinates.get(1).clone(), uvCoordinates.get(2).clone() };
// be careful here, floating point operations might cause the
// texture positions to be inapropriate
int[][] texturePosition = new int[3][2];
for (int i = 0; i < texturePosition.length; ++i) {
texturePosition[i][0] = textureHelper.getPixelPosition(uv[i].x, sourceImage.getWidth());
texturePosition[i][1] = textureHelper.getPixelPosition(uv[i].y, sourceImage.getHeight());
}
// calculating the extent of the texture
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
float minUVX = Float.MAX_VALUE, minUVY = Float.MAX_VALUE;
float maxUVX = Float.MIN_VALUE, maxUVY = Float.MIN_VALUE;
for (int i = 0; i < texturePosition.length; ++i) {
minX = Math.min(texturePosition[i][0], minX);
minY = Math.min(texturePosition[i][1], minY);
maxX = Math.max(texturePosition[i][0], maxX);
maxY = Math.max(texturePosition[i][1], maxY);
minUVX = Math.min(uv[i].x, minUVX);
minUVY = Math.min(uv[i].y, minUVY);
maxUVX = Math.max(uv[i].x, maxUVX);
maxUVY = Math.max(uv[i].y, maxUVY);
}
int width = maxX - minX;
int height = maxY - minY;
if (width == 0) {
width = 1;
}
if (height == 0) {
height = 1;
}
// copy the pixel from the texture to the result image
PixelInputOutput pixelReader = PixelIOFactory.getPixelIO(sourceImage.getFormat());
TexturePixel pixel = new TexturePixel();
ByteBuffer data = BufferUtils.createByteBuffer(width * height * 4);
for (int y = minY; y < maxY; ++y) {
for (int x = minX; x < maxX; ++x) {
int xPos = x >= sourceImage.getWidth() ? x - sourceImage.getWidth() : x;
int yPos = y >= sourceImage.getHeight() ? y - sourceImage.getHeight() : y;
pixelReader.read(sourceImage, 0, pixel, xPos, yPos);
data.put(pixel.getR8());
data.put(pixel.getG8());
data.put(pixel.getB8());
data.put(pixel.getA8());
}
}
image = new Image(Format.RGBA8, width, height, data, ColorSpace.Linear);
// modify the UV values so that they fit the new image
float heightUV = maxUVY - minUVY;
float widthUV = maxUVX - minUVX;
for (int i = 0; i < uv.length; ++i) {
// first translate it to the image borders
uv[i].x -= minUVX;
uv[i].y -= minUVY;
// then scale so that it fills the whole area
uv[i].x /= widthUV;
uv[i].y /= heightUV;
}
}
/**
* Constructor that creates an image element from the 3D texture
* (generated texture). It computes a flat smallest rectangle that can
* hold a (3D) triangle defined by the given UV coordinates. Then it
* defines the image pixels for points in 3D space that define the
* calculated rectangle.
*
* @param faceIndex
* the face index this image refers to
* @param boundingBox
* the bounding box of the mesh
* @param texture
* the texture that allows to compute a pixel value in 3D
* space
* @param uv
* the UV coordinates of the mesh
* @param blenderContext
* the blender context
*/
public TriangleTextureElement(int faceIndex, BoundingBox boundingBox, GeneratedTexture texture, Vector3f[] uv, int[] uvIndices, BlenderContext blenderContext) {
this.faceIndex = faceIndex;
// compute the face vertices from the UV coordinates
float width = boundingBox.getXExtent() * 2;
float height = boundingBox.getYExtent() * 2;
float depth = boundingBox.getZExtent() * 2;
Vector3f min = boundingBox.getMin(null);
Vector3f v1 = min.add(uv[uvIndices[0]].x * width, uv[uvIndices[0]].y * height, uv[uvIndices[0]].z * depth);
Vector3f v2 = min.add(uv[uvIndices[1]].x * width, uv[uvIndices[1]].y * height, uv[uvIndices[1]].z * depth);
Vector3f v3 = min.add(uv[uvIndices[2]].x * width, uv[uvIndices[2]].y * height, uv[uvIndices[2]].z * depth);
// get the rectangle envelope for the triangle
RectangleEnvelope envelope = this.getTriangleEnvelope(v1, v2, v3);
// create the result image
Format imageFormat = texture.getImage().getFormat();
int imageWidth = (int) (envelope.width * blenderContext.getBlenderKey().getGeneratedTexturePPU());
if (imageWidth == 0) {
imageWidth = 1;
}
int imageHeight = (int) (envelope.height * blenderContext.getBlenderKey().getGeneratedTexturePPU());
if (imageHeight == 0) {
imageHeight = 1;
}
ByteBuffer data = BufferUtils.createByteBuffer(imageWidth * imageHeight * (imageFormat.getBitsPerPixel() >> 3));
image = new Image(texture.getImage().getFormat(), imageWidth, imageHeight, data, ColorSpace.Linear);
// computing the pixels
PixelInputOutput pixelWriter = PixelIOFactory.getPixelIO(imageFormat);
TexturePixel pixel = new TexturePixel();
float[] uvs = new float[3];
Vector3f point = new Vector3f(envelope.min);
Vector3f vecY = new Vector3f();
Vector3f wDelta = new Vector3f(envelope.w).multLocal(1.0f / imageWidth);
Vector3f hDelta = new Vector3f(envelope.h).multLocal(1.0f / imageHeight);
for (int x = 0; x < imageWidth; ++x) {
for (int y = 0; y < imageHeight; ++y) {
this.toTextureUV(boundingBox, point, uvs);
texture.getPixel(pixel, uvs[0], uvs[1], uvs[2]);
pixelWriter.write(image, 0, pixel, x, y);
point.addLocal(hDelta);
}
vecY.addLocal(wDelta);
point.set(envelope.min).addLocal(vecY);
}
// preparing UV coordinates for the flatted texture
this.uv = new Vector2f[3];
this.uv[0] = new Vector2f(FastMath.clamp(v1.subtract(envelope.min).length(), 0, Float.MAX_VALUE) / envelope.height, 0);
Vector3f heightDropPoint = v2.subtract(envelope.w);// w is directed from the base to v2
this.uv[1] = new Vector2f(1, heightDropPoint.subtractLocal(envelope.min).length() / envelope.height);
this.uv[2] = new Vector2f(0, 1);
}
/**
* This method computes the final UV coordinates for the image (after it
* is combined with other images and drawed on the result image).
*
* @param totalImageWidth
* the result image width
* @param totalImageHeight
* the result image height
* @param xPos
* the most left x coordinate of the image
* @param yPos
* the most top y coordinate of the image
* @param result
* a vector where the result is stored
*/
public void computeFinalUVCoordinates(int totalImageWidth, int totalImageHeight, int xPos, int yPos, Vector2f[] result) {
for (int i = 0; i < 3; ++i) {
result[i] = new Vector2f();
result[i].x = xPos / (float) totalImageWidth + uv[i].x * (image.getWidth() / (float) totalImageWidth);
result[i].y = yPos / (float) totalImageHeight + uv[i].y * (image.getHeight() / (float) totalImageHeight);
}
}
/**
* This method converts the given point into 3D UV coordinates.
*
* @param boundingBox
* the bounding box of the mesh
* @param point
* the point to be transformed
* @param uvs
* the result UV coordinates
*/
private void toTextureUV(BoundingBox boundingBox, Vector3f point, float[] uvs) {
uvs[0] = (point.x - boundingBox.getCenter().x) / (boundingBox.getXExtent() == 0 ? 1 : boundingBox.getXExtent());
uvs[1] = (point.y - boundingBox.getCenter().y) / (boundingBox.getYExtent() == 0 ? 1 : boundingBox.getYExtent());
uvs[2] = (point.z - boundingBox.getCenter().z) / (boundingBox.getZExtent() == 0 ? 1 : boundingBox.getZExtent());
// UVS cannot go outside <0, 1> range, but since we are generating texture for triangle envelope it might happen that
// some points of the envelope will exceet the bounding box of the mesh thus generating uvs outside the range
for (int i = 0; i < 3; ++i) {
uvs[i] = FastMath.clamp(uvs[i], 0, 1);
}
}
/**
* This method returns an envelope of a minimal rectangle, that is set
* in 3D space, and contains the given triangle.
*
* @param triangle
* the triangle
* @return a rectangle minimum and maximum point and height and width
*/
private RectangleEnvelope getTriangleEnvelope(Vector3f v1, Vector3f v2, Vector3f v3) {
Vector3f h = v3.subtract(v1);// the height of the resulting rectangle
Vector3f temp = v2.subtract(v1);
float field = 0.5f * h.cross(temp).length();// the field of the rectangle: Field = 0.5 * ||h x temp||
if (field <= 0.0f) {
return new RectangleEnvelope(v1);// return single point envelope
}
float cosAlpha = h.dot(temp) / (h.length() * temp.length());// the cosinus of angle betweenh and temp
float triangleHeight = 2 * field / h.length();// the base of the height is the h vector
// now calculate the distance between v1 vertex and the point where
// the above calculated height 'touches' the base line (it can be
// settled outside the h vector)
float x = Math.abs((float) Math.sqrt(FastMath.clamp(temp.lengthSquared() - triangleHeight * triangleHeight, 0, Float.MAX_VALUE))) * Math.signum(cosAlpha);
// now get the height base point
Vector3f xPoint = v1.add(h.normalize().multLocal(x));
// get the minimum point of the envelope
Vector3f min = x < 0 ? xPoint : v1;
if (x < 0) {
h = v3.subtract(min);
} else if (x > h.length()) {
h = xPoint.subtract(min);
}
Vector3f envelopeWidth = v2.subtract(xPoint);
return new RectangleEnvelope(min, envelopeWidth, h);
}
}
/**
* A class that represents a flat rectangle in 3D space that is built on a
* triangle in 3D space.
*
* @author Marcin Roguski (Kaelthas)
*/
private static class RectangleEnvelope {
/** The minimum point of the rectangle. */
public final Vector3f min;
/** The width vector. */
public final Vector3f w;
/** The height vector. */
public final Vector3f h;
/** The width of the rectangle. */
public final float width;
/** The height of the rectangle. */
public final float height;
/**
* Constructs a rectangle that actually holds a point, not a triangle.
* This is a special case that is sometimes used when generating a
* texture where UV coordinates are defined by normals instead of
* vertices.
*
* @param pointPosition
* a position in 3D space
*/
public RectangleEnvelope(Vector3f pointPosition) {
min = pointPosition;
h = w = Vector3f.ZERO;
width = height = 1;
}
/**
* Constructs a rectangle envelope.
*
* @param min
* the minimum rectangle point
* @param w
* the width vector
* @param h
* the height vector
*/
public RectangleEnvelope(Vector3f min, Vector3f w, Vector3f h) {
this.min = min;
this.h = h;
this.w = w;
width = w.length();
height = h.length();
}
@Override
public String toString() {
return "Envelope[min = " + min + ", w = " + w + ", h = " + h + "]";
}
}
@Override
public Texture createSimpleClone() {
return null;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy