如何使用OpenGL制作淡出效果?

5
我试图实现淡出效果,但我不知道该怎么做。我尝试了几种方法,但由于OpenGL的工作原理,它们都失败了。
我将解释它的工作原理:
如果我画一个白色像素,并在每帧向某个方向移动一个像素,那么每帧屏幕像素的R/G/B值就会减少一个(范围为0-255),因此在255帧后,白色像素将完全变黑。因此,如果我移动白色像素,我会看到一个渐变的轨迹,从白色到黑色,每个像素颜色与前一个像素颜色相比均匀地减少1个颜色值。
编辑:我更喜欢非着色器方式来完成这个效果,但如果不可能,我也可以接受使用着色器的方式。
编辑2:由于这里存在一些困惑,我想告诉大家,我已经通过在整个场景上绘制黑色透明矩形来完成这种效果。但是,这并不像我想要的那样工作;像素的黑暗程度有限,因此它总是会留下一些像素“可见”(高于零的颜色值),因为:1*0.9 = 0.9 -> 四舍五入再加1,等等。我可以通过缩短轨迹来“修复”这个问题,但我希望能够尽可能地调整轨迹长度,并且使用线性插值而不是双线性插值(如果这是正确的词),这样它就始终会在0-255范围内将每个r、g、b值减少1,而不是使用百分比值。
编辑3:仍然存在一些困惑,所以让我们明确一下:我想改进通过从glClear()中禁用GL_COLOR_BUFFER_BIT所完成的效果,我不想永远看到屏幕上的像素,因此我想通过在我的场景上绘制一个矩形,逐渐使它们变暗,每次减少1个颜色值(在0-255范围内)。
编辑4:我想要一个OpenGL方法来实现这个效果,该效果应尽可能节省功率、内存或带宽。该效果应在不清除屏幕像素的情况下工作,因此,如果我在我的场景上绘制一个透明的矩形,先前绘制的像素将变暗等。但如上述几次解释,它并不能很好地工作。大的NO有:1)从屏幕读取像素,使用for循环逐个修改它们,然后再上传回去。2)以不同的暗度多次渲染我的对象来模拟轨迹效果。3)乘以颜色值不是一个选项,因为它不会使像素变黑,它们将在某个亮度下永远停留在屏幕上(请参见上面的解释)。

你能写一个简单的测试程序来演示问题吗?可能是你初始化OpenGL或其他方面的问题。 - Nicol Bolas
@Nicol,我发现图形渐变不正确是由于混合函数不正确引起的,请查看我的编辑以了解新问题。 - Rookie
5个回答

9
如果我画一个白色像素,并在每一帧中将其向某个方向移动一个像素,那么每一帧屏幕像素的R/G/B值都会减少一个(范围为0-255),因此经过255帧后,白色像素将完全变黑。因此,如果我移动白色像素,我将看到一个渐变轨迹,从白色到黑色,每个像素颜色与前一个像素颜色相比均匀减少1个颜色值。
在我解释如何做到这一点之前,我想说,你要实现的视觉效果是一个可怕的视觉效果,你不应该使用它。从RGB颜色中减去一个值将产生一个不同的颜色,而不是同一颜色的较暗版本。如果你从RGB颜色(255,128,0)中减去128次1,则会变成(128,0,0)。第一个颜色是棕色,第二个颜色是深红色。这些颜色不相同。
现在,由于你没有很好地解释这个问题,我必须猜测一些东西。我假设您正在渲染的内容中没有“对象”。没有状态。您只是在任意位置绘制东西,您不记得在哪里绘制了什么,也不想记住在哪里绘制了什么。
为了实现您想要的效果,您需要两个离屏缓冲区。我建议使用FBO和屏幕大小的纹理来完成这些操作。基本算法很简单。您将上一帧的图像渲染到当前图像中,使用“减 1”混合模式从您写入的颜色中减去1。然后您将新的内容渲染到当前图像中。然后您显示该图像。之后,您切换哪个图像是前一个,哪个是当前的,并重新执行整个过程。

注意:以下代码将假定OpenGL 3.3功能可用。

初始化

因此,在初始化期间(在OpenGL初始化之后),您必须创建屏幕大小的纹理。您还需要两个屏幕大小的深度缓冲区。

GLuint screenTextures[2];
GLuint screenDepthbuffers[2];
GLuint fbos[2]; //Put these definitions somewhere useful.

glGenTextures(2, screenTextures);
glGenRenderbuffers(2, screenDepthbuffers);
glGenFramebuffers(2, fbos);
for(int i = 0; i < 2; ++i)
{
  glBindTexture(GL_TEXTURE_2D, screenTextures[i]);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, SCREEN_WIDTH, SCREEN_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glBindTexture(GL_TEXTURE_2D, 0);

  glBindRenderbuffer(GL_RENDERBUFFER, screenDepthBuffers[i]);
  glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCREEN_WIDTH, SCREEN_HEIGHT);
  glBindRenderbuffer(GL_RENDERBUFFER, 0);

  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[i]);
  glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, screenTextures[i], 0);
  glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, screenDepthBuffers[i]);
  if(glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    //Error out here.
  }
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}

绘制上一帧

下一步是将上一帧的图像绘制到当前图像中。

为了实现这一点,我们需要有一个先前和当前的FBO概念。这可以通过两个变量:currIndexprevIndex来实现。这些值是指向我们的纹理、渲染缓冲区和FBO的GLuint数组的索引。它们应该在初始化时进行初始化(不是每一帧),如下所示:

currIndex = 0;
prevIndex = 1;

在您的绘制过程中,第一步是绘制上一帧,减去一个(再次强烈建议在此处使用真正的混合)。
这不是完整的代码; 我希望您填写伪代码。
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbos[currIndex]);
glClearColor(...);
glClearDepth(...);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);

glActiveTexture(GL_TEXTURE0 + 0);
glBindTexture(GL_TEXTURE_2D, screenTextures[prevIndex]);
glUseProgram(BlenderProgramObject); //The shader will be talked about later.

RenderFullscreenQuadWithTexture();

glUseProgram(0);
glBindTexture(GL_TEXTURE_2D, 0);

RenderFullscreenQuadWithTexture 函数正如其名称所示:使用当前绑定的纹理渲染一个与屏幕大小相同的四边形。程序对象 BlenderProgramObject 是一个 GLSL 着色器,用于执行混合操作。它从纹理中获取数据并进行混合。我假设您知道如何设置着色器等相关内容。

片段着色器的主函数可能如下所示:

shaderOutput = texture(prevImage, texCoord) - (1.0/255.0);

再次强烈建议这样做:

shaderOutput = texture(prevImage, texCoord) * (0.05);

如果您不知道如何使用着色器,那么您应该学习。但是如果您不想学,那么您可以使用glTexEnv函数获得相同的效果。如果您不知道这些是什么,我建议学习着色器;从长远来看,这会更容易。

像平常一样绘制

现在,您只需像往常一样渲染所有内容即可。只是不要解除FBO的绑定;我们仍然希望将其渲染到其中。

在屏幕上显示渲染图像

通常,您会使用swapbuffer调用来显示渲染结果。但是由于我们渲染到了FBO,所以我们不能这样做。相反,我们必须执行不同的操作。我们必须将图像复制到后台缓冲区,然后交换缓冲区。
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbos[currIndex]);
glBlitFramebuffer(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, SCREEN_WDITH, SCREEN_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
//Do OpenGL swap buffers as normal

切换图片

现在我们需要做一件事情:切换我们正在使用的图片。之前的图片变成当前的,反之亦然:

std::swap(currIndex, prevIndex);

完成。


你只是展示了我的概念实现,这让你看起来像一个吃白食者。做完整个任务的实现有什么意义呢?不留点事情给提问者自己去做,让他们学到点东西呢?顺便说一下,我怀疑提问者想要渲染一个(体积)粒子效果,也许是烟雾。如果是这样,我怀疑他每帧写入数百万的移动像素的计划根本行不通。 - Razzupaltuff
@Nicol,将颜色乘以会导致我之前提到的错误;这就是为什么我不能乘以,否则在特定亮度下屏幕上将永远看到先前帧的像素。另外,如果我从glClear()中禁用GL_COLOR_BUFFER_BIT,我是否需要FBO?我不关心“质量效果”如何,我只关心速度。 - Rookie
@karx11erx,我不是在渲染烟雾,只是一些粒子在飞舞,并留下漂亮的轨迹,同时尽可能地节省GPU的功率和内存。我可能有点夸张了;) - Rookie
@新手:乘法并不能使颜色永久存在。当一个值被不断乘以0.95时,即使其初始值为1.0,最终也会变成零。这同时不会改变实际的颜色。我猜你不需要清除颜色缓冲区,因为你只是要写一个全屏四边形。但我不知道为什么你不关心效果的质量。 - Nicol Bolas
如果你想让像素轨迹中的一个像素在首次渲染后的255帧后消失,你需要每帧将像素的每个颜色通道减少<初始通道值> / 255.0。这样做会使所有通道同时达到零 - 但会需要更多的数据(你需要记住每个像素的适当通道递减值)。 - Razzupaltuff
显示剩余9条评论

6
您可能想使用glBlendFunc(GL_ONE,GL_SRC_ALPHA)来渲染一个带有从1.0到0.0的alpha的黑色矩形。
针对您的评论进行编辑(回复无法适合评论):
使用简单的淡入黑色操作无法根据其年龄淡化单个像素。通常,渲染目标不会“记住”以前帧中绘制到其中的内容。 我可以想到一种方法,通过交替地渲染到一对FBO之一并在其中使用其alpha通道来实现它,但您需要一个着色器。 因此,您将首先渲染包含先前位置处像素的FBO,将其alpha值减少一,在alpha == 0时删除它们,否则每当其alpha减小时使它们变暗,然后以alpha == 255渲染当前位置处的像素。
如果你只有动态的像素:
render FBO 2 to FBO 1, darkening each pixel in it by a scale (skip during first pass)
render moving pixels to FBO 1
render FBO 1 to FBO 2 (FBO 2 is the "age" buffer)
render FBO 2 to screen

如果您想修改某个场景(例如,有一个场景和其中的动态像素),请按照以下步骤操作:
set glBlendFunc (GL_ONE, GL_ZERO)
render FBO 2 to FBO 1, reducing each alpha > 0.0 in it by a scale (skip during first pass)
render moving pixels to FBO 1
render FBO 1 to FBO 2 (FBO 2 is the "age" buffer)
render the scene to screen
set glBlendFunc (GL_ONE, GL_SRC_ALPHA)
render FBO 2 to screen

实际上,比例应该是(float) / 255.0 / 255.0,以使各组件平等地消失(并且不是从较低的值开始变为零,其他组件还没变为零)。

如果您只有少数动态像素,则可以将像素重新渲染在之前的所有位置上,一直到255个“滴答声”。

由于您需要重新渲染每个像素,只需使用正确的颜色渐变对每个像素进行渲染即可:像素越旧,颜色越暗。如果您有大量像素,则双FBO方法可能有效。

我写的是“滴答声”,而不是帧,因为帧可以根据渲染器和硬件花费不同的时间,但您可能希望像素轨迹在恒定时间内消失。这意味着您只需要在若干毫秒后使每个像素变暗,保持它们的颜色在两帧之间。


只有在我不希望在淡出效果之上渲染任何东西的情况下,这个方法才有效...我试图使每个渲染像素的尾迹可见,如果像素移动,它将创建一个逐渐变黑的尾迹。而且总是有一个像素完全可见。每一帧将从RGB颜色中删除一个0-255范围内的值,因此每一帧都会变暗,只是一个值,所以下一帧它将变得更暗两个值,以此类推,最终255帧后就完全变黑了。 - Rookie
  1. 这个FBO解决方案是否需要我上传屏幕_x*屏幕_y数量的像素数据?这对我来说不是一个选项。
  2. 重新渲染像素也不是一个选择(我会有成千上万个像素)。
  3. 如果我让我的像素随着时间变暗,那么一段时间后我将无法看到它们,这不是效果的目的。
  4. 我不关心时间,无论您有多少FPS,我都以相同的方式渲染所有内容。
- Rookie
我在我的问题中添加了更多细节。如果我需要使用着色器,我不认为需要FBO:是否可能通过简单的着色器color -= 1.0f/255.0f;将颜色值减少1?我对着色器和FBO并不是很有经验,所以我可能是错的。但是,如果可能的话,我更喜欢非着色器解决方案。 - Rookie
我不太明白你真正想要什么。你想让移动的像素在它们身后留下一个变暗的轨迹,对吗?如果是这样,请检查我对答案所做的修改。 - Razzupaltuff

2

如果屏幕上只有淡出黑色的效果,那么一种非着色器的做法是通过readpixels将屏幕内容获取到并放入纹理中,然后在屏幕上放置一个矩形,使用该纹理来渲染矩形,接着可以调节矩形的颜色向黑色渐变,以实现所需的效果。


像我上面描述的那样,渲染到FBO可能会更快。 - Razzupaltuff

1

这是驱动程序的问题,Windows本身不支持OpenGL或仅支持低版本,我认为是1.5。所有更新的版本都配备了来自ATI、NVIDIA、Intel等厂商的驱动程序。 您是否使用不同的显卡? 您实际使用的OpenGL版本是什么?


我之前在XP和W7上使用同一张显卡,但我认为驱动程序不同(不确定...),我的OpenGL版本是3.2.9655。尽管如此,我希望能够解决问题,而不必告诉人们更新驱动程序或任何其他操作,除非这是十分罕见的情况,只有百万分之一的人会出现这种问题。请查看我的编辑,我已经在我的帖子中添加了图片。 - Rookie
你会编写OpenGL3吗?拥有最高版本3.2并不意味着你在使用它 :). 使用OpenGL3意味着不使用任何来自以前版本的弃用函数。 - user852830
@scorcher24:“使用OpenGL3意味着不使用来自以前版本的任何弃用函数。” 不是这样的。如果创建兼容性上下文(在不使用wglCreateContextAttribs时默认情况下),则不会失去任何功能。并且您仍然可以在兼容性模式下使用高达4.1(最新版本)。在MacOSX Lion上情况有所不同,但我们在这里谈论的是Windows。 - Nicol Bolas
@Nicol 我们是在谈论向前兼容性吗?那是另一回事。但是,如果您仍在使用已弃用的函数,仅具有报告3.x的上下文是不足以表明您正在使用3.x的。使用3.x意味着坚持3的标准:http://www.opengl.org/sdk/docs/man3/ - user852830
@scorcher24:我在谈论核心与兼容性。两者都不比另一个更“使用3.x”。这个OpenGL 3.3规范是否比这个OpenGL 3.3规范更3.3?(这两个都是PDF链接。)它们都被命名为“OpenGL图形系统:规范”。区别在于一个是核心配置文件,另一个是兼容性配置文件。 - Nicol Bolas
我进行了更多的测试并完全编辑了我的问题,请查看。 - Rookie

1

像这样的情况使我无法使用纯OpenGL。我不确定您的项目是否有空间(如果你正在使用另一个窗口API,可能没有空间),或者添加复杂性是否值得,但是添加一个像SDL这样的2D库可以与OpenGL一起工作,让您直接以合理的方式处理显示表面的像素,以及一般像素,而OpenGL通常并不容易实现。

然后,您只需要在OpenGL渲染其几何图形之前运行显示表面的像素,并从每个RGB组件中减去1。

这是我能想到的最简单的解决方案,如果使用OpenGL的附加库是一个选项的话。


这不是一个选项,因为它会限制我使用800x600分辨率(如果我想要60fps),并且基本上限制我使用任何其他数据流,因为它会完全占用我的显卡带宽。 - Rookie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接