I spent the best part of the day looking into this, because I'm also seeing this exact issue. I think I finally got to the bottom of it.
This is caused by the way libGDX loads images. A texture is created from a Pixmap
on all platforms, where a Pixmap
is basically an in-memory mutable image. This is implemented in the core library with some native code (presumably for speed).
However, since native code is obviously impossible in the browser, Pixmap
has a different implementation in the GWT backend. The salient part there is the constructor:
public Pixmap (FileHandle file) {
GwtFileHandle gwtFile = (GwtFileHandle)file;
ImageElement img = gwtFile.preloader.images.get(file.path());
if (img == null) throw new GdxRuntimeException("Couldn't load image '" + file.path() + "', file does not exist");
create(img.getWidth(), img.getHeight(), Format.RGBA8888);
context.setGlobalCompositeOperation(Composite.COPY);
context.drawImage(img, 0, 0);
context.setGlobalCompositeOperation(getComposite());
}
This creates a HTMLCanvasElement
and a CanvasRenderingContext2D
, then draws the image to the canvas. This makes sense in the libGDX context, since a Pixmap
is supposed to be mutable, but an HTML image is read-only.
I'm not exactly sure how the pixels are eventually retrieved again for upload to the OpenGL texture, but by this point we're doomed already. Because note this warning in the canvas2d spec:
Note: Due to the lossy nature of converting to and from premultiplied alpha color values, pixels that have just been set using putImageData()
might be returned to an equivalent getImageData()
as different values.
To show the effect, I created a JSFiddle: https://jsfiddle.net/gg9tbejf/ This doesn't use libGDX, just raw canvas, JavaScript and WebGL, but you can see that the image is mutilated after a round-trip through canvas2d.
Apparently most (all?) major browsers store their canvas2d data with premultiplied alpha, so lossless recovery is impossible. This SO question shows fairly conclusively that there is currently no way around that.
Edit: I wrote a workaround in my local project without modifying libGDX itself. Create ImageTextureData.java
in your GWT project (package name matters; it accesses package-private fields):
package com.badlogic.gdx.backends.gwt;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.TextureData;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.webgl.client.WebGLRenderingContext;
public class ImageTextureData implements TextureData {
private final ImageElement imageElement;
private final Pixmap.Format format;
private final boolean useMipMaps;
public ImageTextureData(ImageElement imageElement, Pixmap.Format format, boolean useMipMaps) {
this.imageElement = imageElement;
this.format = format;
this.useMipMaps = useMipMaps;
}
@Override
public TextureDataType getType() {
return TextureDataType.Custom;
}
@Override
public boolean isPrepared() {
return true;
}
@Override
public void prepare() {
}
@Override
public Pixmap consumePixmap() {
throw new GdxRuntimeException("This TextureData implementation does not use a Pixmap");
}
@Override
public boolean disposePixmap() {
throw new GdxRuntimeException("This TextureData implementation does not use a Pixmap");
}
@Override
public void consumeCustomData(int target) {
WebGLRenderingContext gl = ((GwtGL20) Gdx.gl20).gl;
gl.texImage2D(target, 0, GL20.GL_RGBA, GL20.GL_RGBA, GL20.GL_UNSIGNED_BYTE, imageElement);
if (useMipMaps) {
gl.generateMipmap(target);
}
}
@Override
public int getWidth() {
return imageElement.getWidth();
}
@Override
public int getHeight() {
return imageElement.getHeight();
}
@Override
public Pixmap.Format getFormat() {
return format;
}
@Override
public boolean useMipMaps() {
return useMipMaps;
}
@Override
public boolean isManaged() {
return false;
}
}
Then add GwtTextureLoader.java
anywhere in your GWT project:
package com.example.mygame.gwt;
import com.badlogic.gdx.assets.AssetDescriptor;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.AsynchronousAssetLoader;
import com.badlogic.gdx.assets.loaders.FileHandleResolver;
import com.badlogic.gdx.assets.loaders.TextureLoader;
import com.badlogic.gdx.backends.gwt.GwtFileHandle;
import com.badlogic.gdx.backends.gwt.ImageTextureData;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.TextureData;
import com.badlogic.gdx.utils.Array;
import com.google.gwt.dom.client.ImageElement;
public class GwtTextureLoader extends AsynchronousAssetLoader<Texture, TextureLoader.TextureParameter> {
TextureData data;
Texture texture;
public GwtTextureLoader(FileHandleResolver resolver) {
super(resolver);
}
@Override
public void loadAsync(AssetManager manager, String fileName, FileHandle fileHandle, TextureLoader.TextureParameter parameter) {
if (parameter == null || parameter.textureData == null) {
Pixmap.Format format = null;
boolean genMipMaps = false;
texture = null;
if (parameter != null) {
format = parameter.format;
genMipMaps = parameter.genMipMaps;
texture = parameter.texture;
}
// Mostly these few lines changed w.r.t. TextureLoader:
GwtFileHandle gwtFileHandle = (GwtFileHandle) fileHandle;
ImageElement imageElement = gwtFileHandle.preloader.images.get(fileHandle.path());
data = new ImageTextureData(imageElement, format, genMipMaps);
} else {
data = parameter.textureData;
if (!data.isPrepared()) data.prepare();
texture = parameter.texture;
}
}
@Override
public Texture loadSync(AssetManager manager, String fileName, FileHandle fileHandle, TextureLoader.TextureParameter parameter) {
Texture texture = this.texture;
if (texture != null) {
texture.load(data);
} else {
texture = new Texture(data);
}
if (parameter != null) {
texture.setFilter(parameter.minFilter, parameter.magFilter);
texture.setWrap(parameter.wrapU, parameter.wrapV);
}
return texture;
}
@Override
public Array<AssetDescriptor> getDependencies(String fileName, FileHandle fileHandle, TextureLoader.TextureParameter parameter) {
return null;
}
}
Then set that loader on your AssetManager
in your GWT project only:
assetManager.setLoader(Texture.class, new GwtTextureLoader(assetManager.getFileHandleResolver()));
Note: You have to ensure that your images are power of two to begin with; this approach can obviously do no conversions for you. Mipmapping and texture filtering options should be supported though.
It would be nice if libGDX would to stop using canvas2d in the common case of just loading an image, and just pass the image element to texImage2D
directly. I'm not sure how to fit that in architecturally (and I'm a GWT noob to boot). Since the original GitHub issue is closed, I've filed a new one with the suggested solution.
Update: the issue was fixed in this commit, which is included in libGDX 1.9.4 and above.
render()
to double check alpha is set to 1. The same problem still occurs. – Will Calderwood