diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput24.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput24.java
index bdc1c5fef7..3dd56d7295 100644
--- a/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput24.java
+++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput24.java
@@ -114,7 +114,7 @@ public void setCursorVisible(boolean visible) {
public void setNativeCursor(JmeCursor cursor) {
if(inputHandler.getView()!=null) {
if(cursor!=null) {
- // Translate into Android Bitmap format ARGB888. Assuming input image as RGBA
+ // Translate into Android Bitmap format ARGB888. Assuming input image as RGBA where each channel is an integer.
int bufferSize = cursor.getHeight()*cursor.getWidth();
int[] outputBitmap=new int[bufferSize];
IntBuffer inputImage = cursor.getImagesData().asReadOnlyBuffer();
diff --git a/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java b/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java
index a70a01bd21..61f57d9dc4 100644
--- a/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java
+++ b/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java
@@ -673,6 +673,25 @@ public int asIntABGR() {
return toInt(a, b, g, r);
}
+ /**
+ * Retrieves the component values of this ColorRGBA as an
+ * int[] of lenght four in r,g,b,a order.
+ * Where:
+ * int[0] = red value in range of 0-255.
+ * int[1] = green value in range of 0-255.
+ * int[2] = blue value in range of 0-255.
+ * int[3] = alpha value in range of 0-255.
+ *
+ * @return The integer array representation of this ColorRGBA in r,g,b,a order.
+ */
+ public int[] asIntsRGBA() {
+ int intR = ((int) (r * 255) & 0xFF);
+ int intG = ((int) (g * 255) & 0xFF);
+ int intB = ((int) (b * 255) & 0xFF);
+ int intA = ((int) (a * 255) & 0xFF);
+ return new int[] {intR, intG, intB, intA};
+ }
+
/**
* Sets the component values of this ColorRGBA with the given
* combined ARGB int.
@@ -824,10 +843,10 @@ private float fromByte(int channelByte) {
* Helper method to combine four float channels into an int.
*/
private int toInt(float c1, float c2, float c3, float c4) {
- int r = ((int) (c1 * 255) & 0xFF);
- int g = ((int) (c2 * 255) & 0xFF);
- int b = ((int) (c3 * 255) & 0xFF);
- int a = ((int) (c4 * 255) & 0xFF);
- return (r << 24) | (g << 16) | (b << 8) | a;
+ int intC1 = ((int) (c1 * 255) & 0xFF);
+ int intC2 = ((int) (c2 * 255) & 0xFF);
+ int intC3 = ((int) (c3 * 255) & 0xFF);
+ int intC4 = ((int) (c4 * 255) & 0xFF);
+ return (intC1 << 24) | (intC2 << 16) | (intC3 << 8) | intC4;
}
}
diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java
index 3181abd4f9..9410ca81e9 100644
--- a/jme3-core/src/main/java/com/jme3/texture/Image.java
+++ b/jme3-core/src/main/java/com/jme3/texture/Image.java
@@ -79,16 +79,16 @@ public enum Format {
Reserved2(0),
/**
- * half-precision floating-point grayscale/luminance.
- *
- * Requires {@link Caps#HalfFloatTexture}.
+ * half-precision floating-point grayscale/luminance.
+ *
+ * Requires {@link Caps#HalfFloatTexture}.
*/
Luminance16F(16,true),
/**
* single-precision floating-point grayscale/luminance.
*
- * Requires {@link Caps#FloatTexture}.
+ * Requires {@link Caps#FloatTexture}.
*/
Luminance32F(32,true),
@@ -101,9 +101,9 @@ public enum Format {
Reserved3(0),
/**
- * half-precision floating-point grayscale/luminance and alpha.
- *
- * Requires {@link Caps#HalfFloatTexture}.
+ * half-precision floating-point grayscale/luminance and alpha.
+ *
+ * Requires {@link Caps#HalfFloatTexture}.
*/
Luminance16FAlpha16F(32,true),
@@ -255,7 +255,7 @@ public enum Format {
* but will be converted to {@link Format#RGB111110F} when sent
* to the video hardware.
*
- * Requires {@link Caps#HalfFloatTexture} and {@link Caps#PackedFloatTexture}.
+ * Requires {@link Caps#HalfFloatTexture} and {@link Caps#PackedFloatTexture}.
*/
RGB16F_to_RGB111110F(48,true),
@@ -271,7 +271,7 @@ public enum Format {
* but will be converted to {@link Format#RGB9E5} when sent
* to the video hardware.
*
- * Requires {@link Caps#HalfFloatTexture} and {@link Caps#SharedExponentTexture}.
+ * Requires {@link Caps#HalfFloatTexture} and {@link Caps#SharedExponentTexture}.
*/
RGB16F_to_RGB9E5(48,true),
@@ -283,9 +283,9 @@ public enum Format {
RGB9E5(32,true),
/**
- * half-precision floating point red, green, and blue.
- *
- * Requires {@link Caps#HalfFloatTexture}.
+ * half-precision floating point red, green, and blue.
+ *
+ * Requires {@link Caps#HalfFloatTexture}.
* May be supported for renderbuffers, but the OpenGL specification does not require it.
*/
RGB16F(48,true),
@@ -293,22 +293,22 @@ public enum Format {
/**
* half-precision floating point red, green, blue, and alpha.
*
- * Requires {@link Caps#HalfFloatTexture}.
+ * Requires {@link Caps#HalfFloatTexture}.
*/
RGBA16F(64,true),
/**
- * single-precision floating point red, green, and blue.
- *
- * Requires {@link Caps#FloatTexture}.
+ * single-precision floating point red, green, and blue.
+ *
+ * Requires {@link Caps#FloatTexture}.
* May be supported for renderbuffers, but the OpenGL specification does not require it.
*/
RGB32F(96,true),
/**
- * single-precision floating point red, green, blue and alpha.
- *
- * Requires {@link Caps#FloatTexture}.
+ * single-precision floating point red, green, blue and alpha.
+ *
+ * Requires {@link Caps#FloatTexture}.
*/
RGBA32F(128,true),
@@ -507,21 +507,21 @@ public enum Format {
/**
* half-precision floating point red.
*
- * Requires {@link Caps#HalfFloatTexture}.
+ * Requires {@link Caps#HalfFloatTexture}.
*/
R16F(16,true),
/**
- * single-precision floating point red.
- *
- * Requires {@link Caps#FloatTexture}.
+ * single-precision floating point red.
+ *
+ * Requires {@link Caps#FloatTexture}.
*/
R32F(32,true),
/**
* half-precision floating point red and green.
*
- * Requires {@link Caps#HalfFloatTexture}.
+ * Requires {@link Caps#HalfFloatTexture}.
*/
RG16F(32,true),
@@ -719,7 +719,8 @@ public long getUniqueId() {
}
/**
- * @return A shallow clone of this image. The data is not cloned.
+ * @return a shallow clone of this image. The data is not cloned.
+ * @see #deepClone() Deep clone.
*/
@Override
public Image clone(){
@@ -731,6 +732,27 @@ public Image clone(){
return clone;
}
+ /**
+ * @return a deep clone of this image. The data is cloned.
+ * @see #clone() Shallow clone.
+ */
+ public Image deepClone() {
+ Image clone = (Image) super.clone();
+ clone.mipMapSizes = mipMapSizes != null ? mipMapSizes.clone() : null;
+ clone.data = data != null ? dataDeepClone() : null;
+ clone.lastTextureState = new LastTextureState();
+ clone.setUpdateNeeded();
+ return clone;
+ }
+
+ private ArrayList dataDeepClone() {
+ ArrayList clone = new ArrayList(data.size());
+ for (ByteBuffer originalBuffer : data) {
+ clone.add(BufferUtils.clone(originalBuffer));
+ }
+ return clone;
+ }
+
/**
* Constructor instantiates a new Image object. All values
* are undefined.
diff --git a/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg b/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg
index b350b696f6..7d339204ec 100644
--- a/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg
+++ b/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg
@@ -1,4 +1,3 @@
INCLUDE com/jme3/asset/General.cfg
# Desktop-specific loaders
-LOADER com.jme3.cursors.plugins.CursorLoader : ani, cur, ico
diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java
new file mode 100644
index 0000000000..7830f97d19
--- /dev/null
+++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (c) 2009-2026 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.cursors.plugins;
+
+import java.nio.IntBuffer;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.jme3.math.ColorRGBA;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture2D;
+import com.jme3.texture.image.ImageRaster;
+import com.jme3.util.BufferUtils;
+
+/**
+ * Convert any image like object to a {@link JmeCursor}.
+ *
+ * This class is configurable by it's constructors, allowing to create
+ * {@link JmeCursor} instances that meet the cursor consumer's necessities.
+ */
+public class CursorConverter {
+ /**
+ * Supported formats for {@link JmeCursor}'s image data.
+ */
+ public enum CursorFormat {
+ /**
+ * One byte (8 bits) for each channel (Alpha, Red, Green, Blue).
+ */
+ ARGB,
+
+ /**
+ * One interger (32 bits) for each channel (Red, Green, Blue, Alpha).
+ */
+ RGBAI
+ }
+
+ private final CursorConverter.CursorFormat outputCursorFormat;
+ private final boolean outputVerticalFlip;
+
+ /**
+ * Creates a {@link CursorConverter} instance.
+ *
+ * @param cursorFormat the {@link JmeCursor}'s image data will follow the
+ * specified image format.
+ * @param verticalFlip if true, the {@link JmeCursor}'s image will be flipped vertically, otherwise
+ * it won't be flipped. It only affect the cursor's image, It doesn't flip hotspots as well.
+ */
+ public CursorConverter(CursorConverter.CursorFormat cursorFormat, boolean verticalFlip) {
+ outputCursorFormat = cursorFormat;
+ outputVerticalFlip = verticalFlip;
+ }
+
+ /**
+ * Convert a {@link Texture2D} to a {@link JmeCursor}.
+ *
+ * The coordinate system used is the same specified in {@link JmeCursor}, the start point is 0, 0 being
+ * lower left.
+ *
+ * The cursor's hot spot will be the top left of the texture's image.
+ *
+ * @param cursorTexture the texture to convert. No modifications will be applied.
+ * @return the {@link JmeCursor} object that contains the data for a cursor.
+ */
+ public JmeCursor fromTexture(Texture2D cursorTexture) {
+ return fromTexture(cursorTexture, 0, cursorTexture.getImage().getHeight() - 1);
+ }
+
+ /**
+ * Convert a {@link Texture2D} to a {@link JmeCursor}.
+ *
+ * The coordinate system used is the same specified in {@link JmeCursor}, the start point is 0, 0 being
+ * lower left.
+ *
+ * @param cursorTexture the texture to convert. No modifications will be applied.
+ * @param xHotspot same specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis
+ * coordinate for its hotspot. Note that the coordinate system is the same as
+ * OpenGL. 0, 0 being lower left. Must be between 0 and image width.
+ * @param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis
+ * coordinate for its hotspot. Note that the coordinate system is the same as
+ * OpenGL. 0, 0 being lower left. Must be between 0 and image height.
+ * @return the {@link JmeCursor} object that contains the data for a cursor.
+ *
+ * @throws IllegalArgumentException if the specified hotspots are outside the image's size.
+ */
+ public JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int yHotspot) {
+ Image image = cursorTexture.getImage();
+
+ int imageHeight = image.getHeight();
+ int imageWidth = image.getWidth();
+
+ if (!hotspotInsideImageLimits(xHotspot, yHotspot, imageWidth, imageHeight)) {
+ throw new IllegalArgumentException(
+ "Cursor's hot spot must be inside image limits (width and height)"
+ );
+ }
+
+ if (outputVerticalFlip) {
+ image = image.deepClone(); //Avoid modifying the original.
+ verticalFlip(image);
+ }
+ IntBuffer imageData = getDataAsIntBuffer(image);
+
+ JmeCursor jmeCursor = new JmeCursor();
+ jmeCursor.setWidth(imageWidth);
+ jmeCursor.setHeight(imageHeight);
+ jmeCursor.setxHotSpot(xHotspot);
+ jmeCursor.setyHotSpot(yHotspot);
+ jmeCursor.setNumImages(1);
+ jmeCursor.setImagesDelay(null);
+ jmeCursor.setImagesData(imageData);
+ return jmeCursor;
+ }
+
+ /**
+ * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor,
+ * interpreting each {@link Texture2D} object as a frame of the animated cursor.
+ *
+ * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being
+ * lower left.
+ *
+ * The cursor's hot spot will be the top left of the texture's image.
+ *
+ * @param cursorFrames the frames that will make up the cursor animation.
+ * No modifications will be applied.
+ * @param frameDelay the time delay that will take for a cursor to change from one frame to another.
+ * @return a {@link JmeCursor} object that contains the data for an animated cursor.
+ *
+ * @throws IllegalArgumentException if images inside cursorFrames array have different sizes.
+ */
+ public JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int frameDelay) {
+ int yHotspot = 0;
+ if (cursorFrames.length > 0) {
+ yHotspot = cursorFrames[0].getImage().getHeight() - 1;
+ }
+ return fromTextureFrames(cursorFrames, frameDelay, 0, yHotspot);
+ }
+
+ /**
+ * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor,
+ * interpreting each {@link Texture2D} object as a frame of the animated cursor.
+ *
+ * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being
+ * lower left.
+ *
+ * The cursor's hot spot will be the top left of the texture's image.
+ *
+ * @param cursorFrames the frames that will make up the cursor animation.
+ * No modifications will be applied.
+ * @param frameDelays the time delay that will take for a cursor to change from one frame to another.
+ * @return a {@link JmeCursor} object that contains the data for an animated cursor.
+ *
+ * @throws IllegalArgumentException if the lenghts of cursorFrames and frameDelays arrays doesn't match.
+ * @throws IllegalArgumentException if images inside cursorFrames array have different sizes.
+ */
+ public JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int[] frameDelays) {
+ int yHotspot = 0;
+ if (cursorFrames.length > 0) {
+ yHotspot = cursorFrames[0].getImage().getHeight() - 1;
+ }
+ return fromTextureFrames(cursorFrames, frameDelays, 0, yHotspot);
+ }
+
+ /**
+ * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor,
+ * interpreting each {@link Texture2D} object as a frame of the animated cursor.
+ *
+ * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being
+ * lower left.
+ *
+ * @param cursorFrames the frames that will make up the cursor animation.
+ * No modifications will be applied.
+ * @param frameDelay the time delay that will take for a cursor to change from one frame to another.
+ * @param xHotspot same specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis
+ * coordinate for its hotspot. Note that the coordinate system is the same as
+ * OpenGL. 0, 0 being lower left. Must be between 0 and image width.
+ * @param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis
+ * coordinate for its hotspot. Note that the coordinate system is the same as
+ * OpenGL. 0, 0 being lower left. Must be between 0 and image height.
+ * @return a {@link JmeCursor} object that contains the data for an animated cursor.
+ *
+ * @throws IllegalArgumentException if images inside cursorFrames array have different sizes.
+ * @throws IllegalArgumentException if the specified hotspots are outside the images's sizes.
+ */
+ public JmeCursor fromTextureFrames(
+ Texture2D[] cursorFrames,
+ int frameDelay,
+ int xHotspot,
+ int yHotspot) {
+ int[] frameRates = new int[cursorFrames.length];
+ Arrays.fill(frameRates, frameDelay);
+ return fromTextureFrames(cursorFrames, frameRates, xHotspot, yHotspot);
+ }
+
+ /**
+ * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor,
+ * interpreting each {@link Texture2D} object as a frame of the animated cursor.
+ *
+ * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being
+ * lower left.
+ *
+ * @param cursorFrames the frames that will make up the cursor animation.
+ * No modifications will be applied.
+ * @param frameDelays the time delay that will take each frame to change to the next frame. Because of it,
+ * it must contains as many delays as frames (lengths of cursorFrames and frameDelays
+ * arrays must be equal).
+ * @param xHotspot tame specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis
+ * coordinate for its hotspot. Note that the coordinate system is the same as
+ * OpenGL. 0, 0 being lower left. Must be between 0 and image width.
+ * @param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis
+ * coordinate for its hotspot. Note that the coordinate system is the same as
+ * OpenGL. 0, 0 being lower left. Must be between 0 and image height.
+ * @return a {@link JmeCursor} object that contains the data for an animated cursor.
+ *
+ * @throws IllegalArgumentException if the lenghts of cursorFrames and frameDelays arrays doesn't match.
+ * @throws IllegalArgumentException if images inside cursorFrames array have different sizes.
+ * @throws IllegalArgumentException if the specified hotspots are outside the images's sizes.
+ */
+ public JmeCursor fromTextureFrames(
+ Texture2D[] cursorFrames,
+ int[] frameDelays,
+ int xHotspot,
+ int yHotspot) {
+ if (frameDelays.length != cursorFrames.length) {
+ throw new IllegalArgumentException(
+ "The lengths of cursorFrames and frameDelays arrays must be equal"
+ );
+ }
+
+ List imageFrames = Arrays.stream(cursorFrames)
+ .map((frame) -> frame.getImage())
+ .collect(Collectors.toList());
+
+ List imageFrameHeights = imageFrames
+ .stream()
+ .map((image) -> image.getHeight())
+ .distinct()
+ .collect(Collectors.toList());
+
+ List imageFrameWidths = imageFrames
+ .stream()
+ .map((image) -> image.getWidth())
+ .distinct()
+ .collect(Collectors.toList());
+
+ if (imageFrameHeights.size() > 1 || imageFrameWidths.size() > 1) {
+ throw new IllegalArgumentException("Some images from the Texture2D objects have different sizes");
+ }
+
+ int imageHeight = imageFrameHeights.get(0);
+ int imageWidth = imageFrameWidths.get(0);
+
+ if (!hotspotInsideImageLimits(xHotspot, yHotspot, imageWidth, imageHeight)) {
+ throw new IllegalArgumentException(
+ "Cursor's hot spot must be inside image limits (width and height)"
+ );
+ }
+
+ IntBuffer imagesData = createImageDataBuffer(imageHeight * imageWidth * cursorFrames.length);
+
+ if (outputVerticalFlip) {
+ imageFrames = imageFrames
+ .stream()
+ .map((image) -> image.deepClone()) //Avoid modifying the original image.
+ .peek((image) -> verticalFlip(image))
+ .collect(Collectors.toList());
+ }
+
+ List framesData = imageFrames
+ .stream()
+ .map((image) -> getDataAsIntBuffer(image))
+ .collect(Collectors.toList());
+
+ for (IntBuffer frameData : framesData) {
+ imagesData.put(frameData);
+ }
+ imagesData.flip();
+
+ JmeCursor jmeCursor = new JmeCursor();
+ jmeCursor.setWidth(imageWidth);
+ jmeCursor.setHeight(imageHeight);
+ jmeCursor.setxHotSpot(xHotspot);
+ jmeCursor.setyHotSpot(yHotspot);
+ jmeCursor.setNumImages(cursorFrames.length);
+ jmeCursor.setImagesDelay(BufferUtils.createIntBuffer(frameDelays));
+ jmeCursor.setImagesData(imagesData);
+ return jmeCursor;
+ }
+
+ private static void verticalFlip(Image image) {
+ int height = image.getHeight();
+ int width = image.getWidth();
+
+ ImageRaster raster = ImageRaster.create(image);
+
+ for (int i = 0; i < width; i++) {
+ for (int j = 0; j < height / 2; j++){
+ ColorRGBA reserve = raster.getPixel(i, j);
+ raster.setPixel(i, j, raster.getPixel(i, height - j -1));
+ raster.setPixel(i, height - j -1, reserve);
+ }
+ }
+ }
+
+ private IntBuffer getDataAsIntBuffer(Image image) {
+ int width = image.getWidth();
+ int height = image.getHeight();
+
+ ImageRaster raster = ImageRaster.create(image);
+ IntBuffer data = createImageDataBuffer(width * height);
+
+ if (outputCursorFormat == CursorFormat.ARGB) {
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ ColorRGBA imagePixel = raster.getPixel(x, y);
+ int cursorPixel = imagePixel.asIntARGB();
+ data.put(cursorPixel);
+ }
+ }
+ } else {
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ ColorRGBA imagePixel = raster.getPixel(x, y);
+ int[] cursorPixel = imagePixel.asIntsRGBA();
+ data.put(cursorPixel);
+ }
+ }
+ }
+
+ data.flip();
+ return data;
+ }
+
+ private boolean hotspotInsideImageLimits(
+ int xHotspot,
+ int yHotspot,
+ int imageWidth,
+ int imageHeight) {
+ return xHotspot >= 0 && xHotspot < imageWidth && yHotspot >= 0 && yHotspot < imageHeight;
+ }
+
+ private IntBuffer createImageDataBuffer(int baseSize) {
+ if (outputCursorFormat == CursorFormat.ARGB) {
+ return BufferUtils.createIntBuffer(baseSize);
+ } else {
+ return BufferUtils.createIntBuffer(baseSize * 4);
+ }
+ }
+}
diff --git a/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java b/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java
index 7d530ad960..b60fa7c1a7 100644
--- a/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java
+++ b/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java
@@ -51,9 +51,15 @@
import javax.imageio.ImageIO;
/**
+ * Supports loading of .ico, .ani, and .cur cursor file formats.
+ *
* Created Jun 5, 2012 9:45:58 AM
* @author MadJack
+ *
+ * @deprecated This class is not cross-platform, and the supported file formats are no longer commonly used.
+ * Use {@link com.jme3.cursors.plugins.CursorConverter} instead.
*/
+@Deprecated
public class CursorLoader implements AssetLoader {
final private static int FDE_OFFSET = 6; // first directory entry offset
diff --git a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java
index 3ba14216d7..d306b6fbff 100644
--- a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java
+++ b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java
@@ -1,16 +1,54 @@
+/*
+ * Copyright (c) 2009-2026 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
package jme3test.gui;
+import java.util.ArrayList;
+import java.util.Arrays;
+
import com.jme3.app.SimpleApplication;
+import com.jme3.cursors.plugins.CursorConverter;
import com.jme3.cursors.plugins.JmeCursor;
-import java.util.ArrayList;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture2D;
/**
* This test class demonstrate how to change cursor in jME3.
- *
- * NOTE: This will not work on Android as it does not support cursors.
+ *
+ * The different methods of {@link com.jme3.cursors.plugins.CursorConverter} will be tested.
+ * Also, note that {@link com.jme3.cursors.plugins.CursorConverter} will be configured
+ * to match LWJGL cursor requirements.
*
* Cursor test
- * @author MadJack
+ * @author MadJack, mondogo24
*/
public class TestCursor extends SimpleApplication {
@@ -32,26 +70,80 @@ public void simpleInitApp() {
// will still be "used" and loaded, you just won't see it on the screen.
inputManager.setCursorVisible(true);
+ // Note that vertical flip might be needed depending of the original
+ // image's format and the consumer of the resulting {@link com.jme3.cursors.plugins.JmeCursor}.
+ CursorConverter cursorConverter = new CursorConverter(
+ CursorConverter.CursorFormat.ARGB,
+ true
+ );
+
/*
* To make jME3 use a custom cursor it is as simple as putting the
- * .cur/.ico/.ani file in an asset directory. Here we use
+ * image file in an asset directory. Here we use
* "Textures/GUI/Cursors".
*
- * For the purpose of this demonstration we load 3 different cursors and add them
- * into an array list and switch cursor every 8 seconds.
+ * For the purpose of this demonstration we load 3 different cursors and
+ * switch cursor every 5 seconds, applying differents configurations with
+ * {@link com.jme3.cursors.plugins.CursorConverter}.
*
- * The first ico has been made by Sirea and the set can be found here:
+ * At date of 2026/06/01:
+ *
+ * The nyan cat cursor has been made by Sirea. Under the Attribution Required (CC by) license:
* http://www.rw-designer.com/icon-set/nyan-cat
*
- * The second cursor has been made by Virum64 and is Public Domain.
+ * The meme face cursor has been made by Virum64. Released to Public Domain.
* http://www.rw-designer.com/cursor-set/memes-faces-v64
*
- * The animated cursor has been made by Pointer Adic and can be found here:
+ * The animated monkey cursor has been made by Pointer Adic. Released to Public Domain:
* http://www.rw-designer.com/cursor-set/monkey
+ *
+ * The three cursor examples have been converted to png format.
+ * Checking and following the license restrictions in the process.
*/
- cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/meme.cur"));
- cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/nyancat.ico"));
- cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/monkey.ani"));
+
+ Image memeImage = ((Image) assetManager.loadAsset("Textures/Cursors/meme.png"));
+ Texture2D memeTexture = new Texture2D(memeImage);
+
+ Image nyancatImage = ((Image) assetManager.loadAsset("Textures/Cursors/nyancat.png"));
+ Texture2D nyancatTexture = new Texture2D(nyancatImage);
+
+ //Shows a cursor with default hot spot.
+ cursors.add(cursorConverter.fromTexture(memeTexture));
+
+ //Shows a cursor with custom hot spot.
+ cursors.add(cursorConverter.fromTexture(nyancatTexture, 10, 10));
+
+ /*
+ * For animated cursors. Each frame must be loaded.
+ *
+ * Differents configuration with the monkey animated cursor will be
+ * showed by changing the frame delays.
+ */
+ String[] monkeyFramePaths = {
+ "Textures/Cursors/monkey/frame_0001.png",
+ "Textures/Cursors/monkey/frame_0002.png",
+ "Textures/Cursors/monkey/frame_0003.png",
+ "Textures/Cursors/monkey/frame_0004.png",
+ "Textures/Cursors/monkey/frame_0005.png",
+ "Textures/Cursors/monkey/frame_0006.png"
+ };
+
+ Texture2D[] monkeyFrames = Arrays.stream(monkeyFramePaths)
+ .map(framePath -> ((Image) assetManager.loadAsset(framePath)))
+ .map(frameImage -> new Texture2D(frameImage))
+ .toArray(Texture2D[]::new);
+
+ //Shows monkey cursor with same frame delay for each frame and default hot spot.
+ cursors.add(cursorConverter.fromTextureFrames(monkeyFrames, 60));
+
+ //Shows monkey cursor with custom frame delay for each frame and default hot spot.
+ cursors.add(cursorConverter.fromTextureFrames(monkeyFrames, new int[]{40, 40, 40, 200, 200, 200}));
+
+ //Shows monkey cursor with same frame delay for each frame and custom hot spot.
+ cursors.add(cursorConverter.fromTextureFrames(monkeyFrames, 150, 5, 7));
+
+ //Shows monkey cursor with custom frame delay for each frame and hot spot.
+ cursors.add(cursorConverter.fromTextureFrames(monkeyFrames, new int[]{40, 200, 40, 200, 40, 200}, 10, 20));
sysTime = System.currentTimeMillis();
inputManager.setMouseCursor(cursors.get(count));
@@ -61,13 +153,13 @@ public void simpleInitApp() {
public void simpleUpdate(float tpf) {
long currentTime = System.currentTimeMillis();
- if (currentTime - sysTime > 8000) {
+ if (currentTime - sysTime > 5000) {
count++;
if (count >= cursors.size()) {
count = 0;
}
sysTime = currentTime;
- // 8 seconds have passed,
+ // Enough time have passed,
// tell jME3 to switch to a different cursor.
inputManager.setMouseCursor(cursors.get(count));
}