使用预乘alpha时,AlphaGL渐变在WebGL中不平滑

时间:2015-07-17 17:00:28

标签: html5 opengl-es libgdx webgl blending

我有一个带有平滑渐变的卡通云的LibGDX游戏。游戏中还有其他具有类似问题的渐变示例,但云是最明显的例子。它们在Android,iOS和游戏的桌面版本上都很好看,但在WebGL版本中,渐变并不是那么平滑。它似乎只是alpha梯度有问题。其他渐变看起来还不错。

我已尝试在Chrome和IE中使用3种不同的设备,并且所有3种设备都会产生相同的结果。您可以在此处找到HTML5版本的测试。

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

我在github上添加了一个示例IntelliJ项目

https://github.com/WillCalderwood/CloudTest

如果您有intelliJ,请克隆该项目,打开build.gradle文件,按Alt-F12,键入gradlew html:superdev,然后浏览到http://localhost:8080/html/

关键代码为render() here

这里的底部图像是桌面版,顶部是WebGL版,两者都在同一硬件上运行。

enter image description here

绘图没有什么巧妙的。这只是对

的调用
    spriteBatch.draw(texture, getLeft(), getBottom(), getWidth(), getHeight());

我正在使用默认着色器,使用预乘alpha填充的纹理,并将混合函数设置为

    spriteBatch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);

这是实际的图像,虽然alpha不是我的包装工所完成的预乘。

enter image description here

有谁知道可能的原因以及我如何解决它?

更新

仅在使用混合模式GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA

时才会出现这种情况

另一次更新

我尝试改变整个游戏以使用非预乘的alpha纹理。我使用Texture Packer可以帮助修复非预乘alpha经常出现的晕圈问题。所有这些在Android和桌面版本中都能正常工作。在WebGL版本中,虽然我获得了平滑的渐变,但我仍然得到一个小的光晕效果,所以我也不能将它用作解决方案。

另一次更新

这是一张新图片。桌面版本位于顶部,网页版本位于底部。左侧是混合模式GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA,右侧是GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA

enter image description here

这是上面左下角图片的缩放版本,增加对比度以显示问题。

enter image description here

我还尝试使用片段着色器来尝试找出正在发生的事情。如果我设置

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

然后渐变是平滑的,但如果我设置

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

然后我得到了条带。这指向了一个精确的问题我相信,因为色彩通道被预乘过程挤进了光谱的较暗端。

3 个答案:

答案 0 :(得分:5)

WebGL将alphas与标准OpenGL略有不同,并且经常会导致问题。

This site很好地解释了这些差异。

  

OpenGL与WebGL的最大区别在于OpenGL渲染   到没有与任何东西合成的后备缓冲区,或者   操作系统的窗口管理器实际上没有与任何东西合成,   所以你的alpha是什么并不重要。

     

WebGL由浏览器与网页和默认值合成   是使用与.png标签相同的预乘alpha   透明度和2d画布标签。*

该网站还为人们面临的典型问题提供了解决方法。它有点牵扯,但应该解决你的问题。

我不会在这里粘贴整篇文章,但我怀疑你最好坚持使用非预乘,并确保在每次渲染后清除alpha通道。该网站更详细。

答案 1 :(得分:3)

我花了大部分时间研究这个问题,因为我也看到了这个问题。我想我终于深究了它。

这是由libGDX加载图像的方式引起的。在所有平台上从Pixmap创建纹理,其中Pixmap基本上是内存中的可变图像。这是在some native code核心库中实现的(大概是为了速度)。

但是,由于浏览器中显然无法使用原生代码,Pixmapa different implementation in the GWT backend。那里的显着部分是构造函数:

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());
}

这会创建一个HTMLCanvasElement和一个CanvasRenderingContext2D,然后将图像绘制到画布上。这在libGDX上下文中是有意义的,因为Pixmap应该是可变的,但HTML图像是只读的。

我不确定最终如何再次检索像素以上传到OpenGL纹理,但到目前为止我们已经注定了。因为请在canvas2d spec

中注明此警告
  

注意:由于转换为预乘alpha颜色值的损失性质,刚刚使用putImageData()设置的像素可能会返回到等效的getImageData()作为不同的价值观。

为了显示效果,我创建了一个JSFiddle:https://jsfiddle.net/gg9tbejf/这不使用libGDX,只使用原始画布,JavaScript和WebGL,但是你可以看到在通过canvas2d往返后图像被肢解。

显然,大多数(所有?)主流浏览器都将其canvas2d数据存储为预乘alpha,因此无法进行无损恢复。 This SO question相当确凿地表明目前无法解决这个问题。

编辑:我在本地项目中编写了一个解决方法,但没有修改libGDX本身。在GWT项目中创建ImageTextureData.java(包名称很重要;它访问包私有字段):

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;
    }
}

然后在GWT项目的任何位置添加GwtTextureLoader.java

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;
    }
}

然后仅在您的GWT项目中的AssetManager上设置该加载程序:

assetManager.setLoader(Texture.class, new GwtTextureLoader(assetManager.getFileHandleResolver()));

注意:您必须确保图像的功率为2;这种方法显然不会为你做任何转换。但是应该支持Mipmapping和纹理过滤选项。

如果libGDX在加载图像的常见情况下停止使用canvas2d并且直接将图像元素传递给texImage2D,那将是很好的。我不确定如何在架构上适应它(我是一个GWT noob来启动)。由于original GitHub issue已关闭,我已向a new one提交了建议的解决方案。

更新:问题已在this commit中修复,该问题包含在libGDX 1.9.4及更高版本中。

答案 2 :(得分:1)

我想知道你是否在某个地方遇到精确问题 - 预乘alpha纹理使颜色通道比原始颜色更暗。

从概念上讲,此过程会将颜色值压缩到颜色范围的底端,这会在您重新编码为8位纹理时导致量化带。我无法解释的是为什么你会在光明和黑暗之间产生振荡,除非这是颜色和alpha通道中不同波段步幅的相互作用。

OpenGL和OpenGL ES 3.0支持sRGB纹理,这可以帮助解决这个问题(实际上它们能够更好地表达光谱暗端的色差,牺牲高亮度值,而眼睛则不太能够对它们进行分析)。不幸的是,这在OpenGL ES 2.0移动设备上并不广泛(因此不在WebGL上,因为WEbGL基于ES 2.0,尽管sRGB扩展可能在某些设备上可用)。