cube格式的LUT滤镜也叫ColorMapFilter在pixi中应用

这篇文章发布于 2023年11月22日,星期三,00:14,归类于 Graphic相关, JS实例。 阅读 10826 次, 今日 19 次 一条评论

 

cube文件示意图

一、关于LUT滤镜

接上篇文章,讲讲各类视频剪辑APP中滤镜效果在Web中的实现。

剪映中的滤镜

这种滤镜其实也分为两大类,一类是算法滤镜,例如高斯模糊,反相,色调变化,本质上是数学计算,例如pixijs/filters这个项目中的ColorMatrixFilter类效果就是通过矩阵变换计算实现的。

这类滤镜的算法固定,使用简单,但是效果单一,不够丰富。

还有一类就是本文要介绍的颜色映射滤镜,也叫LUT滤镜,LUT是Look Up Table的缩写,中文叫查找表,也叫颜色映射表,在pixijs/filters这个项目中使用ColorMapFilter方法实现,这类滤镜效果丰富,细节细腻,可以实现工业电影一般的视觉质感。

LUT滤镜多以.cube或.3dl后缀结尾,有文本和图片两种形式,互相是可以转换的。

文本格式的LUT滤镜文件内容如下:

# Created by Adobe Photoshop CC 2019.0.1
TITLE "Adobe Photoshop CC 2019.0.1"
LUT_3D_SIZE 33
0.000000 0.000000 0.000000
0.000000 0.000000 0.000000
...

包含标题、尺寸和详细的颜色映射信息。

图片格式的LUT滤镜文件多表现为渐变彩色色块,有方的,横的,还有竖的,例如(17*17*17尺寸大小):

LUT滤镜图片示意

需要注意的是,在通常的WebGL JS库中,都是使用横向的LUT滤镜图片,例如pixijs/filters项目中的ColorMapFilter类,使用的就是横向的LUT滤镜图片。

关于LUT滤镜更多的知识可以参考我4年前写的这篇文章:“3D LUT 滤镜颜色映射原理剖析与JS实现”。

二、LUT滤镜在pixi中的应用

说到pixi.js中的滤镜效果,不得不提一下官方这个项目:https://github.com/pixijs/filters

LUT滤镜本质上就是颜色映射,英文称为 color map,因此,需要使用其提供的ColorMapFilter类。

使用示意如下:

import {ColorMapFilter} from '@pixi/filter-color-map';
import {Container} from 'pixi.js';

const container = new Container();
const colorMap = new Image();
colorMap.src = 'foo/bar/colorMap.png';

container.filters = [new ColorMapFilter(colorMap)];

如果是希望直联使用,可以参考:

<script src="./pixi.js"></script>
<script src="./pixi-filters.js"></script>
<script>
const container = new PIXI.Container();
const colorMap = new Image();
colorMap.src = 'foo/bar/colorMap.png';

container.filters = [new PIXI.filters.ColorMapFilter(colorMap)];
</script>

不过支持直联用法的pixi-filters.js的版本好多年没更新了,需要使用旧版的pixi.js,可能会有问题,所以,推荐使用ESM版本的。

<script src="https://cdn.jsdelivr.net/npm/pixi.js@7.x/dist/pixi.min.js"></script>
<script type="module">
    import { ColorMapFilter } from 'https://cdn.jsdelivr.net/npm/@pixi/filter-color-map@5.1.1/+esm'
    ...
</script>

ColorMapFilter的语法

ColorMapFilter类的语法如下:

new PIXI.filters.ColorMapFilter(colorMap: HTMLImageElement | HTMLCanvasElement | PIXI.BaseTexture | PIXI.Texture, nearest: boolean, mix: number) → {}

其中:

colorMap
颜色映射的图像元素,图片元素,canvas元素或者PIXI的纹理都可以。
nearest
是否对colorMap纹理使用NEAREST。
mix
颜色映射使用的百分率,范围是0~1,0就是原图。

从上面的语法可以看出,在PIXI中,只支持图像类型的LUT素材的渲染,然而,很多时候,我们已有的素材都是文本格式的素材,一堆.cube后缀的文件,我从老文件夹里找了个示意素材,喏,Candlelight.cube滤镜

里面是一戳文本,内容如下:

4828行颜色示意

所以,有必要先将文本格式的LUT滤镜转换为图片格式的LUT滤镜才行。

三、Cube文本LUT转图片

cube里面的文本内容,其实就是一堆颜色值,每行三个,分别是RGB三个通道的值,范围是0~1,例如:

0.000000 0.000000 0.000000
0.000000 0.000000 0.000000
0.000000 0.000000 0.000000

就表示连续三个像素点都是纯黑色。

因此,我们只需要将这些内容转换成RGB色值,然后在Canvas元素上进行绘制就可以了。

于是,首先第一步,我们需要解析cube文件,得到颜色映射图片的尺寸和所有的色值,使用此方法即可,参考自这个Github项目

// 解析cube文件的方法
const parseCubeLUT = function (str) {
    if (typeof str !== 'string') {
        str = str.toString();
    }

    var title = null;
    var type = null;
    var size = 0;
    var domain = [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]];
    var data = [];

    var lines = str.split('\n');

    for (var i=0; i<lines.length; i++) {
        var line = lines[i].trim();

        if(line[0] === '#' || line === '') {
            // Skip comments and empty lines
            continue;
        }

        var parts = line.split(/\s+/);

        switch(parts[0]) {
            case 'TITLE':
                title = line.slice(7, -1);
                break;
            case 'DOMAIN_MIN':
                domain[0] = parts.slice(1).map(Number);
                break;
            case 'DOMAIN_MAX':
                domain[1] = parts.slice(1).map(Number);
                break;
            case 'LUT_1D_SIZE':
                type = '1D';
                size = Number(parts[1]);
                break;
            case 'LUT_3D_SIZE':
                type = '3D';
                size = Number(parts[1]);
                break;
            default:
                data.push(parts.map(Number));
        }
    }

    return {
        title: title,
        type: type,
        size: size,
        domain: domain,
        data: data
    };
}

然后,我们需要将这些色值转换为图片,这里使用Canvas元素,代码如下:

// 将色值转换为图片
const lut2Img = function (lutData) {
    const { size, data } = lutData;

    // 根据size创建canvas元素
    const canvas = document.createElement('canvas');
    canvas.width = size * size;
    canvas.height = size;

    // 绘制在Canvas上
    const context = canvas.getContext('2d');
    const imagedata = context.createImageData(canvas.width, canvas.height);
    // 给对应坐标位置的数据设置色值为绿色
    let startX = 0;
    let startY = 0;
    for (var x = 0; x < data.length; x++) {
        // 垂直计算位置
        startY = Math.floor(x / size) % size;
        startX = x % size + size * Math.floor(Math.floor(x / size) / size);

        // index 计算
        var index = 4 * (startY * size * size + startX);

        imagedata.data[index] = data[x][0] * 255;
        imagedata.data[index + 1] = data[x][1] * 255;
        imagedata.data[index + 2] = data[x][2] * 255;
        imagedata.data[index + 3] = 255;
    }
    // 再重绘
    context.putImageData(imagedata, 0, 0);

    return canvas;
}

这里有个很多人都会犯错的细节,那就是默认情况下的cube颜色色值是按照垂直宫格呈现的(见下图),而PIXI中需要的颜色映射图形需要横排,所以如果我们单纯一个for循环执行,会发现最终渲染的色彩滤镜效果和真实的cube滤镜渲染效果是有较大偏差的。

竖排示意

因此,在for循环的时候,需要精准计算每行色值的位置。

最后,我们将上面两个方法结合起来,就可以实现cube文本转图片的功能了。

有个映射图片,滤镜效果自然不在话下了。

四、眼见为实,demo演示

OK,进入大家最关心最需要的部分,演示和源码,这里使用Candlelight.cube这个滤镜示意。

您可以狠狠地点击这里:pixi.js实现lut颜色映射滤镜demo

可以得到如下图所示的效果。

demo滤镜效果示意

速度还是挺快的,webGL渲染果然比2d context速度快多了。

五、结语

在pixi众多滤镜中看到了DisplacementFilter滤镜,这个其实以前介绍过类似的,不过是SVG的相关滤镜,可以实现元素的扭曲效果,有兴趣可以了解下:“深入理解SVG feDisplacementMap滤镜及实际应用

其实,大家只要在Web视觉表现浸染过了,就会发现所谓的滤镜啊,特效啊,都是一个路子下来的,无论是CSS的、SVG,2D canvas或者WebGL,一通百通。

恩……SVG滤镜里面还有几个滤镜元素我还不是很熟悉,我觉得年底前可以抽时间研究下,不留学习漏洞,等全部都通透了,那就是SVG滤镜高手,而前端从业者这么多,SVG滤镜高手寥寥无几,岂不是优势在我了?

优势在我

(本篇完)

分享到:


发表评论(目前一条评论)

  1. dev1说道:

    厉害厉害୧(๑•̀◡•́๑)૭