PreFound:09 - 色彩运算子

/ dousha99

相乘就是相乘,翻译成正片叠底到底是为啥啊!

渲染到 PNG

目前我们一直在输出完整的视频文件。不过在调试过程中,我们通常只会关心某些具体帧的渲染情况,有时甚至可能只是第一帧的渲染结果。

可以简单地修改一下渲染循环——不需要启动 FFmpeg 编码器,只启动 SDL 并渲染指定时间的帧,之后借助 SDL_image 保存到一个 PNG 文件即可。

auto* bufferSurface = SDL_CreateSurfaceFrom(settings.width,
                                            settings.height,
                                            SDL_PIXELFORMAT_RGB24,
                                            sdl.buffer(),
                                            3 * settings.width);
if (bufferSurface == nullptr) {
    spdlog::error("failed to create surface: {}", SDL_GetError());
    return -2;
}

if (!IMG_Save(bufferSurface, outPath.string().c_str())) {
    spdlog::error("failed to save surface: {}", SDL_GetError());
    SDL_DestroySurface(bufferSurface);
    return -3;
}

SDL_DestroySurface(bufferSurface);

得益于我们之前无状态的设计,我们并不需要按顺序渲染每个帧再丢弃不需要的帧,只需要渲染我们关心的帧即可。

颜色算术 (Color math)

颜色算术控制两个图层混叠的方法。在 Photoshop 等图片处理程序中,它是图层的「混合模式」设置背后的实现。

当我们要混叠两个图层时,我们可以将其建模为:对于结果图层的每一个像素点,其颜色值是两个图层对应位置的像素通过某个函数计算出的结果。而设置图层的混叠模式就是指定这个函数具体是什么。

在 SDL 中,颜色算数是通过设置材质的混合模式实现的。比如,要设置图层按照「正常」的透明模式混叠渲染,只需要设置它的混叠模式为 SDL_BLENDMODE_BLEND 即可:

auto* texture = render_frame();
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);

在 SDL 中,传入 SDL_RenderTexture 函数的材质为上层材质,即作为「源」;传入 SDL_SetRendererTarget 的材质为下层材质,即作为「目标」。SDL 的混叠不会生成一个单独的结果材质,而是直接操作下层材质。

需要注意到,SDL 的颜色运算是在浮点 RGB 色彩空间里进行的,取 255 为 1.f.

SDL 原生支持的色彩运算操作有五种:替换、混合、相加、相乘和调制。它们对应以下的色彩运算操作:

混叠模式对应参数颜色运算透明度运算
替换SDL_BLENDMODE_NONE目标 = 源目标 = 源
混合SDL_BLENDMODE_BLEND目标 = (源 * 源透明度) + (目标 * (1 - 源透明度))目标 = 源透明度 + (目标透明度 * (1 - 源透明度))
相加SDL_BLENDMODE_ADD目标 = 目标 + (源 * 源透明度)目标 = 目标
相乘SDL_BLENDMODE_MUL目标 = (源 * 目标) + (目标 * (1 - 源透明度))目标 = 目标
调制SDL_BLENDMODE_MOD目标 = 源 * 目标目标 = 目标

遮罩效果

我们就借助「相乘」混合模式来实现一个遮罩效果吧——通过叠加一个特殊的图层上去,使得底部图层只显示一小部分。

简单地通过一个以透明为底、纯白为顶的图层作为底层,叠加我们的需要的图片上去即可。注意到这里选择遮罩作为底层,是因为我们需要让图片不需要可见的部分透明度为 0. 结合上面的表格可以知道,相乘会采用底图的透明度。

我们同样还可以给遮罩本身加点动画,比如从左到右扫描之类的。

自定义混合模式

但是如果我们把这个效果应用到文字上,就会发现文字会产生原先没有的不透明底。如果选择「相乘」,则会产生一个白底;而选择「调制」则会产生一个黑色底。

明明文字的底本身是透明的,为什么叠加之后会有这个问题呢?

答案是:因为白色色块本身的透明度是 1.0, 相乘之后就会导致原本透明的部分不再透明;相乘出来是白色是因为 (目标 * (1 - 源透明度)) 得到了 1.0 白色;调制出来是黑色则是因为 源 * 目标 得到的是 0.0 黑色。

所以,如果要正确地处理这个透明部分,我们必须从源上取这个透明度。好在 SDL 支持我们自定义混成方式,即通过 SDL_ComposeCustomBlendMode.

这个函数支持我们自定义整个混色流程。设 srcRGB 为上层颜色,dstRGB 为底层颜色,srcA 为上层透明度,dstA 为底层透明度的话,这个函数相当于是允许我们按照如下公式自由调整混色过程:

dstRGB = colorOperation(srcRGB * srcColorFactor, dstRGB * dstColorFactor);
dstA = alphaOperation(srcA * srcAlphaFactor, dstA * dstAlphaFactor); 

其中,srcColorFactor, dstColorFactor, srcAlphaFactor, dstAlphaFactor 可以设置为如下的值:

程序名作用
SDL_BLENDFACTOR_ZERO0
SDL_BLENDFACTOR_ONE1
SDL_BLENDFACTOR_SRC_COLOR上层颜色
SDL_BLENDFACTOR_ONE_MINUS_SRC_COLOR上层反色
SDL_BLENDFACTOR_SRC_ALPHA上层透明度
SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA上层透明度取反
SDL_BLENDFACTOR_DST_COLOR下层颜色
SDL_BLENDFACTOR_ONE_MINUS_DST_COLOR下层反色
SDL_SDL_BLENDFACTOR_DST_ALPHA下层透明度
SDL_BLENDFACTOR_ONE_MINUS_DST_ALPHA下层透明度取反

以及 colorOperationalphaOperation 支持设置为以下值:

程序名作用
SDL_BLENDOPERATION_ADD结果相加
SDL_BLENDOPERATION_SUBTRACT结果相减
SDL_BLENDOPERATION_REV_SUBTRACT结果相减,但是是右边减左边
SDL_BLENDOPERATION_MINIMUM二者取最小值
SDL_BLENDOPERATION_MAXIMUM二者取最大值

并不是所有渲染后端都支持自定义自定义混合模式,比如在软件渲染后端中就完全没法自定义任何混合模式。不过从 SDL 的代码注释里看,Direct3D, OpenGL/OpenGL ES 以及 Vulkan 后端都是完全支持这些模式的,考虑到大多数设备都至少支持其中一个渲染后端,这个方法应该是可以放心用的。

要模仿「调制」混合模式,我们只需要:

auto blend = SDL_ComposeCustomBlendMode(SDL_BLENDFACTOR_DST_COLOR,
                                        SDL_BLENDFACTOR_ZERO,
                                        SDL_BLENDOPERATION_ADD,
                                        SDL_BLENDFACTOR_DST_ALPHA,
                                        SDL_BLENDFACTOR_ZERO,
                                        SDL_BLENDOPERATION_ADD);

这些设置等价于:

目标 = 源 * 目标
目标透明度 = 源透明度 * 目标透明度

源透明度乘以目标透明度,只要其中一个是 0 就会得到透明效果了:

那,正片叠底?

我没有找到可靠来源,只能说这玩意就是这么翻译的。也许这也是和句柄一样是一个从另外的语境中借调过来的词。

但不管怎么说,这比「颜色增值」要好的多——两个不大于 1 的数相乘,结果一定是不大于这两个数的,「增」到哪去了?而且再怎么说 multiply 也应该翻译成「增殖」吧。

下一步

下一步我们会探索一个比较困难的内容:打字效果。

这个东西看起来简单,实际做起来则要困难很多。它需要照顾很多奇妙的小细节,而且我们会给自己上很大的本可以不上的强度。希望我们做的这堆东西能够有一个有趣的结果吧!

正在加载评论……

发表评论

您的评论将由管理员审核后方可公开显示。

Your comments will be submitted to a human moderator and will only be shown publicly after approval.