MPC-BE播放器里的着色器 其一

在 MPC-BE 中,着色器分为预调整大小像素着色器(pre-resize pixel shaders)和后调整大小像素着色器(pose-resize pixel shaders)两个应用方式。其实这里的中文并不是很合适,以至于让本就有门槛的着色器在实际使用难度上雪上加霜。我直到本周才猜测到了两者的区别,且作为分享:

注意正文基本从本人编写着色器的测试推理而来,没有任何技术资料的支持,也没有阅读项目的代码

着色器可以对画面进行调整,但是输入的画面的分辨率(宽、高方向的像素数量)和输出的分辨率是完全相同的,不能增加,也不能减少。

如果视频文件的原始分辨率与播放器输出画面的分辨率不同,一定会发生缩放、裁切等操作。我认为缩放和裁切是分开应用的,而不是一次性应用。我暂且把视频文件的原始分辨率称为原始分辨率,缩放后的分辨率称为渲染分辨率(大多数视频都会发生缩放),最终播放器窗口显示的裁切后的画面的分辨率(也很可能并没发生裁切)称为显示分辨率。缩放的操作即“调整大小像素(resize pixel)”。

所谓的预调整大小像素着色器(pre-resize pixel shaders)即缩放等操作前,对每个像素的颜色所做的调整;后调整大小像素着色器(pose-resize pixel shaders)即缩放操作后,对每个像素的颜色所做的调整。弄明白这一点,我更愿意把两种应用方式称为:缩放前着色器、缩放后着色器,或者简称为前处理着色器、后处理着色器。

那么这两者有差别嘛?答案是只要发生过缩放操作,就必然有差别,并且在效果、效率上都有差别。首先是效率,如果原始分辨率高于渲染分辨率,前处理着色器需要处理的像素多,资源开销就大;如果原始分辨率低于渲染分辨率,前处理着色器需要处理的像素少,资源开销就低。

其次是效果。缩放过程会产生信息损失,用通俗直观的讲法就是变模糊。对色彩微调的着色器作为前处理着色器,输出结果会更自然;无关视频内容的着色器作为后处理着色器,效果可以更加清晰锐利。

最后是编写着色器的技巧。从我已知信息看,着色器无法同时获取原始分辨率和渲染分辨率——前处理着色器只能获取原始分辨率,后处理着色器只能获取渲染分辨率。实际上对大部分着色器来说,如果需要使用着色器缩放画面,只有前处理着色器可以对原始分辨率的宽高比做判断(即宽高比不同则产生不同的效果);但是受分辨率限制,如果试图缩小画面,使用前处理着色器一定会产生信息损失;当文件分辨率低于输出画面时,尺寸差异越大,损失越明显。当文件分辨率高于输出画面时,在效果上看差异不大。

3 个赞

看晕了 :face_with_spiral_eyes:,我还是 potplayer 摆烂吧

不搞着色器也一样能用,但是着色器还是很有趣的

此帖不再赘述如何应用一个着色器。

如何写或者修改一个着色器呢?从我的经验看,可以非常简单的上手。着色器语法基本就是C语言,有一些扩展,也有一些不被支持。譬如C语言常用的位运算,0b形式的二进制int,着色器都不支持。

MPC-BE自带编辑和调试工具,只需要打开一个视频,右键-着色器-编辑着色器,然后选择一个预设着色器开始修改,点击应用就可以预览效果了。或者点击菜单-新建一个着色器,MPC-BE可以自动生成一个模板:

sampler s0 : register(s0);
float4 p0 :  register(c0);
float4 p1 :  register(c1);

#define width   (p0[0])
#define height  (p0[1])
#define counter (p0[2])
#define clock   (p0[3])
#define one_over_width  (p1[0])
#define one_over_height (p1[1])
#define PI acos(-1)

float4 main(float2 tex : TEXCOORD0) : COLOR {
	float4 c0 = tex2D(s0, tex);

	return c0;
}

这个模板实际上已经可以直接工作了,只是只能原样输出而不做任何处理。

范例中都做了什么事情呢?
首先是从运行环境中获取了几个变量:

sampler s0 : register(s0); // 可以理解为着色器处理之前的图像
float4 p0 :  register(c0); //  不需要理会,用于后边取值
float4 p1 :  register(c1); //  不需要理会,用于后边取值

#define width   (p0[0])  // 从p0中获取宽度,前处理着色器即原始分辨率的宽度,后处理着色器即渲染分辨率的宽度
#define height  (p0[1]) // 从p0中获取高度,前处理着色器即原始分辨率的高度,后处理着色器即渲染分辨率的高度
#define counter (p0[2]) // 
#define clock   (p0[3]) // 时间信息
#define one_over_width  (p1[0]) // width的倒数
#define one_over_height (p1[1]) // height的倒数
#define PI acos(-1) // pi

Main函数做了什么呢?把输出图像的每个像素的x、y坐标输入到main函数,输出这个像素的颜色。

// 输入的tex包含tex.x tex.y两个值,即需要获取的像素的x、y坐标,坐标范围0-1
float4 main(float2 tex : TEXCOORD0) : COLOR {
// float4类型相当于float c[4]这样一个数组,前3个值RGB,每个值的范围0-1
// tex2D是从s0中获取坐标位tex位置的色彩。
	float4 c0 = tex2D(s0, tex);

	return c0;
}

很难顶的是我并没找到很多现成的着色器,并且我对编程并不是很在行。
除近期发的几个帖子外,刚刚又做了一个显示文字到视频画面表面的demo

事实上,shader不可以做太过复杂的运算,因为每帧每个像素点运算一次,运算量是比较大的。所以在视频表面显示文字的最佳选择可能还是特效字幕。

但是,对多数人而言,并没丰富的编程经验(比如我),那么如何做debug查看事实的变量值呢?我觉得直接叠加输出到屏幕上也未尝不可。(实际上可能真的没什么卵用)
作为一些技术积累,仅供参考:

sampler s0 : register(s0);
float4 p0 : register(c0);
float4 p1 : register(c1);

#define width (p0[0])
#define height (p0[1])
#define counter (p0[2])
#define clock (p0[3])
#define one_over_width (p1[0])
#define one_over_height (p1[1])
#define PI acos(-1)

#define s_x 15      // x scale
#define s_y 10      // y scale
#define leaning 0.2 // >=0

float4 main(float2 tex : TEXCOORD0) : COLOR {
  int p[17] = {
      31599, 4681,  29671, 29647, 23497,
      31183, 31215, 29257, 31727, 31689, // 0-9
      1,                                 // .
      1040,                              // :
      32767,                             // black
      16384, 24576, 28672, 30720,        // top 4 pix
  };

  int x;
  if (leaning > 0)
    x = (int)((tex.x * width + leaning * tex.y * height % (4 * s_x)) / s_x);
  else
    x = (int)((tex.x * width) / s_x);

  int y = (int)(tex.y * height / s_y);

  int w_x = x / 4;
  int w_y = y / 6;

  float4 c0 = tex2D(s0, tex);
  if (w_x == 0 && leaning > 0)
    return c0;
    
  int d_x = x % 4;
  int d_y = y % 6;
  if (d_y == 5 || d_x == 3)
    return c0;

  int b = exp2(14 - 3 * d_y - d_x);
  int w = w_x % 17;
  if (p[w] / b % 2 == 1)
    return float4(1, 0, 0, 0);

  return c0;
}

由于mpc-be在调试和保存的过程中,中文注释可能导致文件被破坏,因此没有添加中文注释。大致情况如下:

首先我使用了点阵显示字符的方式,假设每个字符为3*5的点阵(足够显示常见英数字符了),如果用ASCII ART绘制一个0,假设1绘制图像,0是透明的,是这样的:

1,1,1,
1,0,1,
1,0,1,
1,0,1,
1,1,1,

如果把他去掉分割符号,可以看作一个int。把若干按顺序排列的int保存为int数组,按需取出,即可完成文字绘制。

绘制的时候读取了如下参数:


#define s_x 15      // x scale  文字横向放大比例
#define s_y 10      // y scale  文字纵向放大比例
#define leaning 0.2 // >=0,文字倾斜程度

在楼上的技术基础上 ,显示了同一个视频同样全屏播放,两种应用模式下着色器获取到的width和height信息。

我在调整窗口尺寸时,发现信息不会刷新。并且证明了前处理着色器中width height是原始分辨率,后处理着色器中的width height是屏幕分辨率。也就是说,很可能着色器是无法获取当前窗口尺寸的

// $MinimumShaderProfile: ps_3_0
sampler s0 : register(s0);
float4 p0 : register(c0);
float4 p1 : register(c1);

#define width (p0[0])
#define height (p0[1])
#define counter (p0[2])
#define clock (p0[3])
#define one_over_width (p1[0])
#define one_over_height (p1[1])
#define PI acos(-1)

#define s_x 10    // x scale
#define s_y 10    // y scale
#define leaning 0 // >=0

float4 main(float2 tex : TEXCOORD0) : COLOR {
  int p[17] = {
      31599, 4681,  29671, 29647, 23497,
      31183, 31215, 29257, 31727, 31689, // 0-9
      1,                                 // .
      1040,                              // :
      32767,                             // black
      16384, 24576, 28672, 30720,        // top 4 pix
  };

  int x;
  if (leaning > 0)
    x = (int)((tex.x * width + leaning * tex.y * height % (4 * s_x)) / s_x);
  else
    x = (int)((tex.x * width) / s_x);

  int y = (int)(tex.y * height / s_y);

  int w_x = x / 4;
  int w_y = y / 6;

  float4 c0 = tex2D(s0, tex);
  if (w_x == 0 && leaning > 0)
    return c0;

  int i = -1;
  if (w_y == 0) {
    int l = (int)log10(width);
    if (w_x > l)
      return c0;
    i = width / pow(10, l - w_x) % 10;

  } else if (w_y == 1) {
    int l = (int)log10(height);
    if (w_x > l)
      return c0;
    i = height / pow(10, l - w_x) % 10;
  }

  if (i < 0)
    return c0;

  int d_x = x % 4;
  int d_y = y % 6;
  if (d_y == 5 || d_x == 3)
    return c0;

  int b = exp2(14 - 3 * d_y - d_x);
  if (p[i] / b % 2 == 1)
    return float4(1, 0, 0, 0);

  return c0;
}