我有一个带有平滑渐变的卡通云的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版,两者都在同一硬件上运行。
绘图没有什么巧妙的。这只是对
的调用 spriteBatch.draw(texture, getLeft(), getBottom(), getWidth(), getHeight());
我正在使用默认着色器,使用预乘alpha填充的纹理,并将混合函数设置为
spriteBatch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
这是实际的图像,虽然alpha不是我的包装工所完成的预乘。
有谁知道可能的原因以及我如何解决它?
更新
仅在使用混合模式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
这是上面左下角图片的缩放版本,增加对比度以显示问题。
我还尝试使用片段着色器来尝试找出正在发生的事情。如果我设置
gl_FragColor = vec4(c.a, c.a, c.a, 1.0);
然后渐变是平滑的,但如果我设置
gl_FragColor = vec4(c.r, c.r, c.r, 1.0);
然后我得到了条带。这指向了一个精确的问题我相信,因为色彩通道被预乘过程挤进了光谱的较暗端。
答案 0 :(得分:5)
WebGL将alphas与标准OpenGL略有不同,并且经常会导致问题。
This site很好地解释了这些差异。
OpenGL与WebGL的最大区别在于OpenGL渲染 到没有与任何东西合成的后备缓冲区,或者 操作系统的窗口管理器实际上没有与任何东西合成, 所以你的alpha是什么并不重要。
WebGL由浏览器与网页和默认值合成 是使用与.png标签相同的预乘alpha 透明度和2d画布标签。*
该网站还为人们面临的典型问题提供了解决方法。它有点牵扯,但应该解决你的问题。
我不会在这里粘贴整篇文章,但我怀疑你最好坚持使用非预乘,并确保在每次渲染后清除alpha通道。该网站更详细。
答案 1 :(得分:3)
我花了大部分时间研究这个问题,因为我也看到了这个问题。我想我终于深究了它。
这是由libGDX加载图像的方式引起的。在所有平台上从Pixmap
创建纹理,其中Pixmap
基本上是内存中的可变图像。这是在some native code核心库中实现的(大概是为了速度)。
但是,由于浏览器中显然无法使用原生代码,Pixmap
有a 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扩展可能在某些设备上可用)。