相乘就是相乘,翻译成正片叠底到底是为啥啊!
渲染到 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_ZERO | 0 |
SDL_BLENDFACTOR_ONE | 1 |
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 | 下层透明度取反 |
以及 colorOperation 和 alphaOperation 支持设置为以下值:
| 程序名 | 作用 |
|---|---|
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 也应该翻译成「增殖」吧。
下一步
下一步我们会探索一个比较困难的内容:打字效果。
这个东西看起来简单,实际做起来则要困难很多。它需要照顾很多奇妙的小细节,而且我们会给自己上很大的本可以不上的强度。希望我们做的这堆东西能够有一个有趣的结果吧!
正在加载评论……