如何使用 OpenGL 制作渐变到黑色的效果?
Posted
技术标签:
【中文标题】如何使用 OpenGL 制作渐变到黑色的效果?【英文标题】:How to make fading-to-black effect with OpenGL? 【发布时间】:2011-07-25 00:03:05 【问题描述】:我正在尝试实现淡入淡出的效果,但我不知道该怎么做。我尝试了几件事,但由于 opengl 的工作原理,它们都失败了
我将解释它是如何工作的:
如果我绘制 1 个白色像素并将其围绕每个帧移动一个像素到某个方向,则每帧屏幕像素将减少一个 R/G/B 值(范围为 0-255),因此在 255 帧后白色像素将全黑。因此,如果我移动白色像素,我会看到从白色到黑色的渐变轨迹,与之前的像素颜色相比,颜色值差异均匀。
编辑:我更愿意知道非着色器的方式,但如果不可能,那么我也可以接受着色器方式。
Edit2:由于这里有些混乱,我想说我已经可以通过在整个场景上绘制一个黑色透明四边形来实现这种效果。但是,这不起作用,因为我希望它起作用;像素可以获得的黑暗度是有限制的,所以它总是会让一些像素“可见”(高于零颜色值),因为:1*0.9 = 0.9 -> 再次四舍五入为 1,等等。我可以“修复“这是通过缩短轨迹来实现的,但我希望能够尽可能地调整轨迹长度,而不是双线性(如果这是正确的话)插值我想要线性(所以它总是会从每个 r 中减少 -1, g,b 值在 0-255 范围内,而不是使用百分比值)。
Edit3: 仍然有些混乱,所以让我们明确一点:我想通过从 glClear() 禁用 GL_COLOR_BUFFER_BIT 来改善效果,我不想永远看到屏幕上的像素,所以我想通过在我的场景上绘制一个四边形来使它们及时变暗,这将使每个像素的颜色值减少 1(在 0-255 范围内)。
Edit4:我会简单点,我想要 OpenGL 方法来实现,效果应该使用尽可能少的功率、内存或带宽。这种效果应该在不清除屏幕像素的情况下工作,所以如果我在我的场景上绘制一个透明的四边形,之前绘制的像素会变得更暗等。但正如上面几次解释的那样,它不能很好地工作。最大的NO是:1)从屏幕读取像素,在for循环中一一修改,然后上传回来。 2) 用不同的暗度渲染我的对象 X 次以模拟轨迹效果。 3)乘以颜色值不是一种选择,因为它不会使像素变成黑色,它们会以一定的亮度永远留在屏幕上(参见上面的解释)。
【问题讨论】:
你能写一个简单的测试程序来演示这个问题吗?这很可能是您初始化 OpenGL 的方式或其他方式。 @Nicol,我注意到图形褪色不正确是由不正确的混合功能引起的,请检查我的编辑是否有新问题。 【参考方案1】:如果我绘制 1 个白色像素并将其围绕每个帧移动一个像素到某个方向,则每帧屏幕像素将减少一个 R/G/B 值(范围为 0-255),因此在 255 帧后白色像素将全黑。因此,如果我移动白色像素,我会看到从白色到黑色的渐变轨迹,与之前的像素颜色相比,颜色值差异均匀。
在我解释如何做到这一点之前,我想说的是,你想要的视觉效果是一种糟糕的视觉效果,你不应该使用它。从每种 RGB 颜色中减去一个值将产生 不同的颜色,而不是相同颜色的较暗版本。 RGB 颜色 (255,128,0) 减去 1 128 次,将变为 (128, 0, 0)。第一种颜色是棕色,第二种是深红色。这些不一样。
现在,由于您还没有很好地解释这一点,我必须做出一些猜测。我假设您正在渲染的内容中没有“对象”。没有状态。你只是在任意位置画东西,你不记得你在哪里画了什么,你也不想记住在哪里画了什么。
要做你想做的事,你需要两个屏幕外缓冲区。我建议为这些使用FBOs 和屏幕大小的纹理。基本算法很简单。您使用从您编写的颜色中“减去 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 的概念。这是通过有两个变量来完成的:currIndex
和 prevIndex
。这些值是纹理、渲染缓冲区和 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 函数获得相同的效果。如果你不知道这些是什么,我建议learning shaders;从长远来看,这要容易得多。
正常绘制东西
现在,您只需像平常一样渲染所有内容。只是不要取消绑定 FBO;我们仍然想渲染它。
在屏幕上显示渲染图像
通常,您会使用 swapbuffer 调用来显示渲染结果。但由于我们渲染到 FBO,我们不能这样做。相反,我们必须做一些不同的事情。我们必须将我们的图像 blit 到后台缓冲区,然后交换缓冲区。
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);
你已经完成了。
【讨论】:
你只是在展示我的概念的实现;让你看起来像个吃白饭的人。完成任务的完整实施而不是让 OP 去做一些事情以让他学习一些东西有什么意义?顺便说一句,我怀疑 OP 想要渲染(体积)粒子效果,也许是烟雾。如果是这样,我怀疑他每帧写入数百万个移动像素的计划是否会奏效。 @Nicol,将颜色相乘会产生我所说的错误;这就是为什么我不能相乘,否则我会在屏幕上以一定的亮度永远看到前一帧像素。另外,如果我从 glClear() 禁用 GL_COLOR_BUFFER_BIT 是否需要 FBO?我不在乎它的“质量效果”如何,我只关心速度。 @karx11erx,我没有渲染烟雾,只是一些粒子飞来飞去,留下漂亮的痕迹,同时在 GPU 中使用尽可能少的功率和内存。百万我可能有点夸大了;) @Rookie:乘法不会使颜色“永远”存在。 1.0 的值,当不断乘以 0.95 时,最终将变为 0。它会在不改变实际颜色的情况下这样做。我想你可以在不清除颜色缓冲区的情况下离开,因为你只是要写一个全屏四边形。而且我不知道你为什么不关心效果的质量。 如果您希望像素轨迹中的一个像素在第一次渲染后消失 255 帧,您需要将像素的每个颜色通道减少 / 255.0,每帧.这将使所有通道同时达到零 - 但需要更多数据(您需要记住每个像素的正确通道减量)。【参考方案2】:您可能希望使用 glBlendFunc (GL_ONE, GL_SRC_ALPHA) 渲染一个 alpha 从 1.0 到 0.0 的黑色矩形。
根据您的评论进行编辑(回复不适合评论):
您不能通过简单的淡入黑操作根据其年龄淡化单个像素。通常,渲染目标不会“记住”在之前的帧中绘制到它的内容。我可以想办法通过交替渲染到一对 FBO 中的一个并使用它们的 alpha 通道来实现这一点,但是你需要一个着色器。因此,您要做的是首先渲染包含先前位置的像素的 FBO,将其 alpha 值减一,当 alpha == 0 时将其丢弃,否则每当其 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 范围内的值,从而使其每帧变暗,只有一个值,因此下一帧它将变暗 2 个值,依此类推,最终 255 帧后它是全黑的。跨度> 1) 这个 FBO 解决方案需要我上传 screen_x*screen_y 数量的像素数据吗?这不是我的选择。 2)重新渲染像素也不是一个选项(我将拥有数百万个像素)。 3)如果我使我的像素及时变暗,那么一段时间后我将无法看到它们,这不是效果应该做的。 4)我不在乎时间,无论你有什么 FPS,我都会以相同的方式渲染所有内容。 我为我的问题添加了更多细节。如果我需要使用着色器,我认为我不需要 FBO:是否可以使用像color -= 1.0f/255.0f;
这样的简单着色器将颜色值减少 1?我对着色器和 FBO 没有那么丰富的经验,所以我可能是错的。但是,如果可能的话,我更喜欢非着色器解决方案。
我不太明白你真正想要什么。您希望移动像素在它们身后留下变暗的痕迹,对吗?如果是这样,请检查我对答案所做的修改。【参考方案3】:
执行此操作的一种非着色器方式,特别是如果淡入黑色是屏幕上唯一发生的事情是通过readpixels
iirc 获取屏幕内容,将它们弹出到纹理中,然后放入使用该纹理在屏幕上绘制一个矩形,然后您可以将矩形的颜色调制为黑色,以实现您想要完成的效果。
【讨论】:
像我上面描述的那样渲染到 FBO 可能会快得多。【参考方案4】:是驱动,windows本身不支持OpenGL或者只有低版本,我想是1.5。所有较新的版本都带有来自 ATI 或 NVIDIA、Intel 等的驱动程序。 你是用不同的卡吗? 您有效使用的是哪个版本的 OpenGL?
【讨论】:
我在 XP 和 W7 上使用相同的卡,但我认为驱动程序不同(不确定...),我的 opengl 版本是 3.2.9655。但是,我仍然希望我可以在不告诉人们更新驱动程序或任何东西的情况下解决问题,除非这是非常罕见的情况,只发生在百万分之一的人身上。查看我的编辑,我在帖子中添加了图片。 好吧,你会编程 OpenGL3 吗?拥有最高版本 3.2 并不意味着您使用它:)。使用 OpenGL3 意味着不使用 prev 中的任何不推荐使用的函数。版本。 @scorcher24:“使用 OpenGL3 意味着不使用以前版本中的任何已弃用函数。”不,它没有。如果您创建一个兼容性上下文(这是不使用 wglCreateContextAttribs 时的默认设置),那么您不会丢失任何功能。并且您仍然可以使用最高兼容的 4.1(最新版本)。 MacOSX Lion 上的情况有所不同,但我们在这里讨论的是 Windows。 @Nicol 我们在谈论前向兼容性吗?那是不同的东西。但是,如果您仍然使用不推荐使用的函数,那么仅仅拥有一个报告 3.x 的上下文就不会使用 3.x。使用 3.x 意味着坚持 3 的标准:opengl.org/sdk/docs/man3 @scorcher24:我说的是core vs. compatibility。两者都没有比另一个更“使用 3.x”。 this OpenGL 3.3 specification 是否比 this OpenGL 3.3 specification 高 3.3? (这两个都是 PDF 链接。)它们的标题都是“OpenGL 图形系统:规范”。不同之处在于一个用于核心配置文件,另一个用于兼容性配置文件。【参考方案5】:正是这种情况导致我无法使用纯 OpenGL。我不确定你的项目是否有空间(如果你使用另一个窗口 API 可能没有空间),或者增加的复杂性是否值得,但是添加一个像 SDL 这样与 OpenGL 一起使用的 2D 库可以让你以合理的方式直接处理显示表面的像素,以及一般的像素,OpenGL 通常不容易做到这一点。
那么您需要做的就是在 OpenGL 渲染它的几何图形之前遍历显示表面的像素,然后从每个 RGB 分量中减去 1。
这是我能看到的最简单的解决方案,如果可以选择使用带有 OpenGL 的其他库。
【讨论】:
这不是一个选项,因为它会将我限制为 800x600 分辨率(如果我想要 60fps)并且基本上限制我使用任何其他数据流,因为它会将我的带宽挂在 gfx 卡上完全。以上是关于如何使用 OpenGL 制作渐变到黑色的效果?的主要内容,如果未能解决你的问题,请参考以下文章