如何用OpenGL制作渐黑效果

How to make fading-to-black effect with OpenGL?

本文关键字:何用 OpenGL      更新时间:2023-10-16

我想实现渐黑效果,但我不知道怎么做。我尝试了几件事,但由于opengl的工作方式,他们失败了

我将解释它是如何工作的:

如果我画一个白色像素,并在每一帧移动一个像素到某个方向,每一帧屏幕像素将得到一个R/G/B值减少(范围0-255),因此在255帧后白色像素将是完全黑色的。因此,如果我移动白色像素,我将看到一条从白色到黑色的渐变轨迹,与之前的像素颜色相比,颜色相差1个颜色值。

编辑:我宁愿知道这样做的非着色器方式,但如果它不可能,那么我也可以接受着色器方式。

Edit2:由于这里有一些混乱,我想告诉我,我可以通过在我的整个场景中绘制一个黑色透明的四边形来实现这种效果。但是,这并不工作,因为我想它的工作;像素的暗度是有限制的,所以它总是会让一些像素"可见"(高于零颜色值),因为:1*0.9 = 0.9 ->再次四舍五入为1,等等。我可以通过缩短轨迹来"修复"这一点,但我希望能够尽可能地调整轨迹长度,而不是双线性(如果这是正确的词)插值,我想要线性(所以它总是从每个r,g,b值在0-255范围内减少-1,而不是使用百分比值)。

Edit3:仍然有些混乱,所以让我们清楚:我想提高的效果是通过禁用GL_COLOR_BUFFER_BIT从glClear(),我不想看到我的屏幕上的像素永远,所以我想使他们更暗的时间,通过画一个四角在我的场景,将减少每个像素的颜色值1(在0-255比例)。

Edit4:我将使它简单,我想要OpenGL方法,这个效果应该使用尽可能少的电源,内存或带宽。这个效果应该在不清除屏幕像素的情况下工作,所以如果我在场景上画一个透明的四边形,之前画的像素会变暗等等。但正如上面几次解释的那样,它并没有很好地工作。最大的NO是:1)从屏幕读取像素,在for循环中逐个修改它们,然后上传回来。2)用不同的黑暗渲染我的物体X次,以模拟轨迹效果。3)倍增颜色值不是一个选项,因为它不会使像素变成黑色,它们将永远在屏幕上保持一定的亮度(见上面某处的解释)。

如果我画一个白色像素,并在每一帧移动一个像素到某个方向,每一帧屏幕像素将得到一个R/G/B值减少(范围0-255),因此在255帧后白色像素将是完全黑色的。因此,如果我移动白色像素,我将看到一条从白色到黑色的渐变轨迹,与之前的像素颜色相比,颜色相差1个颜色值。

在我解释如何做到这一点之前,我想说的是,你要的视觉效果是一个可怕的视觉效果,你不应该使用它。从每个RGB颜色中减去一个值将产生不同的颜色,而不是相同颜色的更深版本。RGB颜色(255,128,0),如果将它减去1 128次,将变成(128,0,0)。第一种颜色是棕色,第二种是暗红色。这些是不一样的

现在,既然你没有很好地解释这个,我不得不做一些猜测。我假设在你渲染的东西中没有"对象"。没有状态。你只是在任意的位置画东西,你不记得你在哪里画了什么,你也不想记住你在哪里画了什么。

要做你想做的,你需要两个屏幕外缓冲区。我建议使用fbo和屏幕大小的纹理。基本算法很简单。将前一帧的图像渲染到当前图像,使用混合模式,从所写的颜色中"减去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,我们就不能那样做了。相反,我们必须做一些不同的事情。我们必须将图像移到backbuffer中,然后再交换buffer。

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

你可能想用glBlendFunc (GL_ONE, GL_SRC_ALPHA)渲染一个alpha值从1.0到0.0的黑色矩形。

编辑回复你的评论(回复不适合评论):

你不能用一个简单的变黑操作根据它们的年龄来褪色单个像素。通常,渲染目标不会"记住"在前一帧中绘制到它的内容。我可以想到一种方法,交替渲染到一对fbo中的一个,并使用它们的alpha通道,但你需要一个着色器。所以你要做的是首先渲染包含像素在其先前位置的FBO,将其alpha值减少1,在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个"tick"。

因为你需要重新渲染每一个像素,只要渲染每一个适当的颜色梯度:越暗,越老的像素。如果你有很多像素,那就采用双FBO方法可能会奏效。

我写的是刻度,而不是帧,因为帧可能会根据渲染器和硬件花费不同的时间,但你可能想让像素轨迹在恒定的时间内消失。这意味着你只需要在几毫秒后调暗每个像素,并在中间的帧中保持它们的颜色。

一种非着色器的方法,特别是如果渐黑是屏幕上唯一的东西,就是通过readpixels iirc抓取屏幕的内容,将这些内容弹出到纹理中,并使用该纹理在屏幕上放置一个矩形,然后你可以调整矩形的颜色以接近黑色来完成你想要完成的效果。

这是驱动程序,Windows本身不支持OpenGL或只有一个低版本,我想是1.5。所有较新的版本都带有来自ATI或NVIDIA, Intel等的驱动程序。你使用不同的卡吗?你使用的是哪个版本的OpenGL ?

这是这样的情况,所以我不能使用纯OpenGL。我不确定你的项目是否有足够的空间(如果你使用另一个窗口API,可能就没有),或者增加的复杂性是否值得,但是添加一个像SDL这样的2D库(与OpenGL一起工作)将允许你以合理的方式直接处理显示表面的像素,以及一般的像素,这是OpenGL通常不容易做到的。

那么你所需要做的就是在OpenGL渲染它的几何之前遍历显示表面的像素,并从每个RGB组件中减去1。

这是我能看到的最简单的解决方案,如果使用额外的库与OpenGL是一个选项。