html5 - Alpha gradients not smooth in WebGL when using premultiplied alpha -


i've got libgdx game cartoon clouds smooth gradient. there other examples of gradients in game have similar issue, clouds obvious example. fine in android, on ios , on desktop version of game, on webgl version gradients not drawn smooth. appears alpha gradients have problem. other gradients ok.

i've tried on 3 different devices in chrome , ie, , 3 produce same results. can find test of html5 version here.

https://wordbuzzhtml5.appspot.com/canvas/

i've added example intellij project on github here

https://github.com/willcalderwood/cloudtest

if have intellij, clone project, open build.gradle file, press alt-f12, type gradlew html:superdev , browse http://localhost:8080/html/

the critical code render() here

the bottom image here desktop version, top webgl version, both running on same hardware.

enter image description here

there's nothing clever going on drawing. it's call to

    spritebatch.draw(texture, getleft(), getbottom(), getwidth(), getheight()); 

i'm using default shader, textures packed premultiplied alpha blend function set as

    spritebatch.setblendfunction(gl20.gl_one, gl20.gl_one_minus_src_alpha); 

this actual image, although alpha not premultiplied that's done packer.

enter image description here

does know possible reason , how might resolve it?

update

this appears happen when using blending mode gl20.gl_one, gl20.gl_one_minus_src_alpha

another update

i've tried changing whole game use non-premultiplied alpha textures. use texture packer can fix halo issues occur non-premultiplied alpha. works fine in android , desktop version. in webgl version, while smooth gradients, still small halo effect, can't use solution either.

and update

here's new image. desktop version on top, web version on bottom. blending mode gl20.gl_one, gl20.gl_one_minus_src_alpha on left , gl20.gl_src_alpha, gl20.gl_one_minus_src_alpha on right

enter image description here

here's zoomed version of bottom left image above increased contrast show issue.

enter image description here

i've done lot of playing fragment shader try , work out what's happening. if set

gl_fragcolor = vec4(c.a, c.a, c.a, 1.0); 

then gradient smooth, if set

gl_fragcolor = vec4(c.r, c.r, c.r, 1.0); 

then banding. points towards precision issue believe colour channels have been squeezed darker end of spectrum pre-multiplication process.

i spent best part of day looking this, because i'm seeing exact issue. think got bottom of it.

this caused way libgdx loads images. texture created pixmap on platforms, pixmap in-memory mutable image. implemented in core library some native code (presumably speed).

however, since native code impossible in browser, pixmap has a different implementation in gwt backend. salient part there 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 not exist");     create(img.getwidth(), img.getheight(), format.rgba8888);     context.setglobalcompositeoperation(composite.copy);     context.drawimage(img, 0, 0);     context.setglobalcompositeoperation(getcomposite()); } 

this creates htmlcanvaselement , canvasrenderingcontext2d, draws image canvas. makes sense in libgdx context, since pixmap supposed mutable, html image read-only.

i'm not sure how pixels retrieved again upload opengl texture, point we're doomed already. because note warning in canvas2d spec:

note: due lossy nature of converting , premultiplied alpha color values, pixels have been set using putimagedata() might returned equivalent getimagedata() different values.

to show effect, created jsfiddle: https://jsfiddle.net/gg9tbejf/ doesn't use libgdx, raw canvas, javascript , webgl, can see image mutilated after round-trip through canvas2d.

apparently (all?) major browsers store canvas2d data premultiplied alpha, lossless recovery impossible. this question shows conclusively there no way around that.


edit: wrote workaround in local project without modifying libgdx itself. create imagetexturedata.java in gwt project (package name matters; 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 not use pixmap");     }      @override     public boolean disposepixmap() {         throw new gdxruntimeexception("this texturedata implementation not use 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 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;             }              // 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 loader on assetmanager in gwt project only:

assetmanager.setloader(texture.class, new gwttextureloader(assetmanager.getfilehandleresolver())); 

note: have ensure images power of 2 begin with; approach can no conversions you. mipmapping , texture filtering options should supported though.

it nice if libgdx stop using canvas2d in common case of loading image, , pass image element teximage2d directly. i'm not sure how fit in architecturally (and i'm gwt noob boot). since original github issue closed, i've filed a new one suggested solution.

update: issue fixed in this commit, included in libgdx 1.9.4 , above.


Comments