楼主: dasasdhba

[讨论] 【记录向】Shader 学习日记

[复制链接]

36

主题

720

回帖

13

精华

版主

经验
7350
硬币
1157 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2022-12-19 21:05:11 | 显示全部楼层
[22] 描边与发光
这类效果实际上是基于边缘检测(参考 [20])的进一步应用,边缘检测可以简单理解为检测灰度的“跳跃”程度,如果某个区域发生灰度“突变”,就会将其判定为边缘。
不过这一节我们讨论的并不是基于传统边缘检测的描边与发光效果,而是仅仅基于 Alpha 值的边缘测试以及描边和发光。

1. 直接轮廓描边
可以通过一些简单的办法直接获取描边的轮廓,举例:
  • 将原 texture 放大,再减去原 texture。
  • 将原 texture 朝八个方向移动若干像素并叠加,再减去原 texture。

这样的办法可以相对灵活地对轮廓进行各类着色操作,但缺点也很显著,一方面是性能问题,另一方面圆滑度也不一定理想。

2. 检测 Alpha 值
一种自然的想法是直接比较周围像素的 Alpha 值,对于存在周围像素透明度低于阈值的点就判定为边界点,但这样会带来如下麻烦:
  • 阈值应该取多少?
  • 对于半透明的边界点,能直接作不透明描边吗?
  • 对于描边宽度较大的情形,能保证平滑度吗?

对于前两个问题,可以强行将半透明的边界点改为不透明,由此阈值取 0 即可(by WSW);对于后一个问题,就没什么好办法了。

因此我们需要换个新的思路(想想卷积),下面我们给出一个类似边缘检测的办法:
  • 求像素点周围像素 Alpha 的平均值((1/n^2)E(n) 卷积核),记为 α
  • 内发光:采用像素点的原透明度(记为 α0),并用 α 值作为系数叠加某种发光色,即:
    color.rgb += glow_color * α
  • 外发光:叠加透明度为 α 的发光色,同时使用 (1 - α0) 防止内部发光,即:
    color += (1.0 - α0) * vec4(glow_color, α)
  • 描边:在某种发光模式的基础上,将 α 值用 smoothstep 等插值办法处理一下即可。


注:使用 (1/n^2)E(n) 卷积核显然不是唯一的选择,仿照高斯模糊,我们也可以尝试使用特定的加权平均等办法。

注 2:我前段时间转写的 Glow Outline 跟这种方法其实差不多,两层 for 循环实际上就是在作卷积核,感兴趣的可以去看看。
Moonstruck Blossom
个人网站:dasasdhba.github.io

36

主题

720

回帖

13

精华

版主

经验
7350
硬币
1157 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2023-2-15 11:50:36 | 显示全部楼层
本帖最后由 dasasdhba 于 2023-2-15 11:53 编辑

[23] 风格化
在 [20] 中简要介绍了锐化和浮雕的实现,它们都是直接基于卷积实现,下面介绍几个常见的直接修改颜色的风格化效果,原理将不作详解。

1. 单色 Monochrome
使用灰度公式,令新的 RGB 都等于:
0.299*R + 0.587*G + 0.114*B
即可

2. 老照片 Old Photo
R = 0.393*R + 0.769*G + 0.189*B
G = 0.349*R + 0.686*G + 0.168*B
B = 0.272*R + 0.534*G + 0.131*B

3. 铅笔画 Pencil
基于边缘检测和单色效果的综合运用
参考源代码:https://github.com/nightyan/Shad ... aders/Pencil.shader

下作简单讲解
首先作简单边缘检测,在上述源代码中采用了卷积核:
-0.5, -1.0, 0.0
-1.0, 0.0, 1.0
0.0, 1.0, 0.5

接下来我们对 RGB 作单色处理。

不过需要注意,边缘检测实质上在计算颜色的“跳跃程度”,因此结果可能为负。之前已经提到,对此我们可以采取 clamp 方式,但容易预见的是这样我们会丢失很多细节。而如果是直接的视觉呈现,取绝对值也不失为不错的选择,因此我们接下来对负值取绝对值。

到这里就差不多了,只是铅笔画应该是轮廓为黑,背景为白,所以我们最后再反转一下即可。

Moonstruck Blossom
个人网站:dasasdhba.github.io

36

主题

720

回帖

13

精华

版主

经验
7350
硬币
1157 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2023-2-17 16:36:14 | 显示全部楼层

[24] 图像变形
注:本节内容为原创。

下面我们简要介绍图像变形类 Shader。
需要注意,由于 Fragment Shader 的输入区域与输出区域必然相同,对于一些需要将原图像放大至区域外的情况,往往需要人为处理 Texture 使其留空。

1. 输出区域变形
我们熟知 Fragment Shader 处理图像依赖于取值范围为 [0, 1]^2 的 UV 坐标系,这是一个简单的正方形区域。现假设我们需要将图像变形到一个新的区域 A,这就需要找到一映射 f:R^2→R^2,使得 f([0, 1]^2)=A。

在实际应用中,我们只需要找到 f^(-1),然后根据新像的 UV,由 f^(-1) 求出原像 UV 即可。

不难看出,f 的选择往往会非常之多,且不同的选择最后得到的图像效果也往往大相径庭。而一般而言,对于任意给定的 A,并没有什么通用的好办法去处理,下面介绍两类较为局限的办法。

(1) 正交放缩法
这种办法最为简单直接,但适用场合与效果也非常局限,其原理简单来说就是对于一个点,我们直接从两个正交的方向将其压缩到 A 的边界内。下面我们简单讨论水平方向放缩,其他方向同理。

我们将 A 的边界(∂A)写为方程形式 y = g(x),若任给 y0,方程 y0 = g(x) 都有两个解 x1<x2,那么取 f 为将 U 从 [0, 1] 直接压缩至 [x1, x2] 即可。

可以预见,尽管这种方案能够快速达到变形效果,但最后的图像效果仅仅只是拉伸变形,且对于上述提到的方程有超过两个解的情况难以处理。

(2) 坐标变换法
若 A 容易用两个参数 (x, y) 来表示,直接考虑 f^(-1) 可能会更方便,这是因为 f^(-1) 的像是简单的 1*1 正方形,因此我们作简单的坐标变换就能够得到 f^(-1)。

事实上,在 A 可由 [0, 1]^2 经过线性变换得到时,这种办法将会非常有效。而对于非线性的情况,往往不容易处理,举例来说,我们知道半径为 R 的圆域有极坐标表示:
(r, θ), r∈[0, R], θ∈[0, 2π]
假定原点在中心,它们是容易求出的:
r = distance(uv, vec2(0.5))
θ = arctan((uv.y - 0.5)/(uv.x - 0.5))
接下来我们只需要将 (r, θ) 压缩置 [0, 1]^2,也就是:
f^(-1)(r, θ) = (r/R, θ/(2π))

这样我们就将图像映到了圆域,但是效果会与第一种方法大不相同,因为我们直接将极坐标用于了直角坐标;而为了得到类似第一种方法的效果,我们可以将 (r, θ) 首先映射到原像中相同原点的极坐标上,然后再将其转化为直角坐标:
(r, θ)→(r/R*√2/2, θ)
然后取:
(x, y) = vec2(0.5) + r*vec2(cosθ, sinθ)
这样可能超界,超界部分可采用第一种办法处理。

以上两种办法可以勉强用于处理任意给定的 A,然而事实上,我们需要的 A 往往具有特殊的性质(比如对于圆而言,由于其特殊的对称性,事实上任给一个点,我们都能以其到圆心的方向将其压缩至目标圆内),从而可以特殊化处理。这里我们不打算介绍太多特殊情形,具体问题具体分析往往是最好的选择。

2. 三维投影变形
三维投影变形即将二维图像折叠至三维空间,再将结果投影至二维的变形方法。区别于输出区域变形,为了表现出三维的视觉效果,我们还需要适当地调整明暗以体现光照。

其大体思路是以垂直于 UV 平面的方向再建立一坐标轴 W,不妨取屏幕向里方向为正,然后采用下列步骤:
a. 将 UV 空间折叠至 UVW 空间,这需要找到相关映射 f
b. 取新 UVW 空间的 UV 投影,这是容易的
c. 根据新 UVW 空间上的 W 值为颜色叠加明暗变化(参考 [6])

下面举两个 a 中相关映射 f 的简单例子:

(1) 平面旋转
将 UV 平面绕 U 轴或 V 轴旋转,以绕 U 轴为例,只需要给 VW 坐标乘上旋转矩阵(需要注意,W 初值应为 0)即可。

此外,亦可以作部分旋转(即折叠操作),比如以 U = 0.5 为转轴,旋转 U > 0.5 的部分。

(2) 圆柱面
以竖直方向的圆柱为例,固定 V,我们只需要将 UW 映射到 UW 平面的圆上。已知周长为 1,由此不难计算得半径为 R=1/(2π),不妨取圆心为 P(0.5, *, 1/(2π))。
接下来利用极坐标,将 [0, 1] 映射至 [0, 2π],考虑到圆心的位置,我们希望将 U = 0.5 映射到 π,这恰好是:θ = U*2π
(如果圆心不取在中心位置,则此处需要作平移处理)

进而容易由极坐标求出新的 UW:
(U, W) = P + R*(cosθ, sinθ)

如你所见,这个半径太小了,出于视觉效果的考虑,我们一般不将 UV 平面卷成圆柱,而是直接给定一个弧度 α 或半径 R,利用弧长 = 1 去计算极角 θ 的取值范围,从而类似处理。

3. 局部图像变形
此类变形并不改变图像输出区域,而是直接对图像作变形,扭曲等操作。这种变形效果往往是将同一类操作不均匀地作用于图像得到的,下面介绍一些例子:

(1) 不均匀模糊(参考 [21])
将高斯模糊应用于图像,但是越接近中心的点模糊半径越大。

(2) 不均匀缩放(输出区域变形)
将图像放大,但是越接近中心的点放大倍率越大。

(3) 不均匀旋转
将图片旋转,但是越接近中心的点旋转角度越小。(漩涡效果)

相信不难从这几个例子中看出此类方法的共性,而具体实现在此就不作过多介绍了。
Moonstruck Blossom
个人网站:dasasdhba.github.io

36

主题

720

回帖

13

精华

版主

经验
7350
硬币
1157 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2023-3-1 19:38:13 | 显示全部楼层
本帖最后由 dasasdhba 于 2023-6-27 14:39 编辑

[25] 图像放大算法
下面我们介绍游戏常用的放大算法
再重复一遍,Fragment Shader 不能改变 Texture 本身尺寸,为了得到放大后的结果,一般采取与 Vertex Shader 协助的办法(参考 [9]),因为无论是人为留空还是提前临近处理都会带来不必要的麻烦。

1. HQnx
2003 年就被提出的比较老的算法,实现的不算很漂亮,甚至找不到什么原理解析,下面给出我的个人理解。
HQnx 用于像素风格图像的放大,其中横线和竖线的放大是容易的,关键在于对斜线放大的处理。

(1). 检测斜线
我们简单地考虑每个像素周围 3*3 的区域,不考虑镜像,斜线可分为两类:

a. 双边线
■□□
□■□
□□■
b. 单边线
■■■
□■■
□□■

若考虑镜像则有 6 类。

为了检测斜线,我们使用特定的 3*3 卷积核对灰度(参考 [23])作卷积,这需要对上述黑方格■部分的灰度求均值并与原像素灰度比较,在差距不超过预设的某个阈值时就将其判定为某种类型的斜线。

(2). 使用模板
对于每种类型的斜线,我们将其处理为预先准备好的放大模板。如上述 a 情形可以使用模板:
■■▲□□
■■■▲□
▲■■■▲
□▲■■■
□□▲■■
其中▲表示半透明混合(参考 [8])

然而,在具体实现上,主流的做法是采用 YUV 的方法获取权重,并利用 LUT 查表得到最后的 filter 值,可参考:https://github.com/CrossVR/hqx-shader/blob/master/glsl/hq2x.glsl

2. xBRZ
2011 年被提出的算法,基本思路是首先作边缘检测,然后判定每一个角落的像素是属于直角还是圆角,并基于这一点采取不同的插值达到较为合理的效果。

因此具体如何边缘检测和判定角落类型会比较复杂,好在这个算法比较热门,已经有比较好的具体解析可供参考:https://www.luogu.com.cn/blog/ud2/xbrz-interpolation-explained

3. Super SAI
众所周知的 MF 的 Texture 采用的放大算法,于 1999 年即被提出,这个东西其实挺冷门的,但考虑到圈内应用广泛,我将其从 C# 版本(来自 imageresizer,这玩意其实是开源的)转写为了 GLSL 版本(可在 The book of Shader 网站上测试),并附带了相关注释,可供参考(但其实我也看不懂):
  1. #ifdef GL_ES
  2. precision mediump float;
  3. #endif

  4. uniform sampler2D u_tex0;
  5. uniform vec2 u_tex0Resolution;

  6. uniform vec2 u_resolution;

  7. // 用于 rgb 转 yuv
  8. const mat3 yuv_matrix = mat3(
  9.     0.299, -0.169, 0.5,
  10.     0.587, -0.331, -0.419,
  11.     0.114, 0.5,    -0.081);
  12. const vec3 yuv_threshold = vec3(48.0/255.0, 7.0/255.0, 6.0/255.0);
  13. const vec3 yuv_offset = vec3(0, 0.5, 0.5);

  14. // 通过 yuv 判断两个颜色的相似度
  15. bool like(vec4 c1, vec4 c2) {
  16.     vec3 a = yuv_matrix * c1.rgb;
  17.     vec3 b = yuv_matrix * c2.rgb;

  18.     bvec3 res = greaterThan(abs((a + yuv_offset) - (b + yuv_offset)), yuv_threshold);
  19.         return !(res.x || res.y || res.z);
  20. }

  21. // 一个莫名其妙的函数
  22. // a 与 c d 相似返回 -1
  23. // a 不与 c d 相似且 b 与 c d 相似返回 1
  24. // 其他情况返回 0
  25. float cond(vec4 a, vec4 b, vec4 c, vec4 d) {
  26.     bool ac = like(a, c);
  27.     bool ad = like(a, d);
  28.     bool bc = like(b, c);
  29.     bool bd = like(b, d);

  30.     float x = float(ac) + float(ad);
  31.     float y = float(bc && !ac) + float(bd && !ad);
  32.     return float(x <= 1.0) - float(y <= 1.0);
  33. }

  34. void main () {
  35.     vec2 uv = gl_FragCoord.xy/u_resolution.xy;
  36.     vec2 unit = 1.0/u_tex0Resolution;

  37.     // c0  c1  c2  d3
  38.     // c3  c4  c5  d4
  39.     // c6  c7  c8  d5
  40.     // d0  d1  d2  d6
  41.     // 其中 c4 在当前 UV 位置
  42.     vec4 c0 = texture2D(u_tex0, uv + vec2(-1.0, -1.0)*unit);
  43.     vec4 c1 = texture2D(u_tex0, uv + vec2( 0.0, -1.0)*unit);
  44.     vec4 c2 = texture2D(u_tex0, uv + vec2( 1.0, -1.0)*unit);
  45.     vec4 d3 = texture2D(u_tex0, uv + vec2( 2.0, -1.0)*unit);
  46.     vec4 c3 = texture2D(u_tex0, uv + vec2(-1.0,  0.0)*unit);
  47.     vec4 c4 = texture2D(u_tex0, uv + vec2( 0.0,  0.0)*unit);
  48.     vec4 c5 = texture2D(u_tex0, uv + vec2( 1.0,  0.0)*unit);
  49.     vec4 d4 = texture2D(u_tex0, uv + vec2( 2.0,  0.0)*unit);
  50.     vec4 c6 = texture2D(u_tex0, uv + vec2(-1.0,  1.0)*unit);
  51.     vec4 c7 = texture2D(u_tex0, uv + vec2( 0.0,  1.0)*unit);
  52.     vec4 c8 = texture2D(u_tex0, uv + vec2( 1.0,  1.0)*unit);
  53.     vec4 d5 = texture2D(u_tex0, uv + vec2( 2.0,  1.0)*unit);
  54.     vec4 d0 = texture2D(u_tex0, uv + vec2(-1.0,  2.0)*unit);
  55.     vec4 d1 = texture2D(u_tex0, uv + vec2( 0.0,  2.0)*unit);
  56.     vec4 d2 = texture2D(u_tex0, uv + vec2( 1.0,  2.0)*unit);
  57.     vec4 d6 = texture2D(u_tex0, uv + vec2( 2.0,  2.0)*unit);

  58.     // e00 e01
  59.     // e10 e11
  60.     // 放大后输出的四个像素的颜色
  61.     vec4 e00 = c4;
  62.     vec4 e01 = c4;
  63.     vec4 e10;
  64.     vec4 e11 = c4;

  65.     // 处理 e01 与 e11
  66.     if (like(c7, c5) && !like(c4, c8)) {
  67.         vec4 c57 = mix(c7, c5, 0.5);
  68.         e11 = c57;
  69.         e01 = c57;
  70.     } else if (like(c4, c8) && !like(c7, c5)) {
  71.         // pass
  72.     } else if (like(c4, c8) && like(c7, c5)) {
  73.         vec4 c57 = mix(c7, c5, 0.5);
  74.         vec4 c48 = mix(c4, c8, 0.5);

  75.         // 谁能告诉我它在干嘛???
  76.         float conc = 0.0;
  77.         conc += cond(c57, c48, c6, d1);
  78.         conc += cond(c57, c48, c3, c1);
  79.         conc += cond(c57, c48, d2, d5);
  80.         conc += cond(c57, c48, c2, d4);

  81.         if (conc > 0.0) {
  82.             e11 = c57;
  83.             e01 = c57;
  84.         } else if (conc == 0.0) {
  85.             e11 = mix(c48, c57, 0.5);
  86.             e01 = e11;
  87.         }
  88.     } else {
  89.         // 地狱绘图.jpg
  90.         if (like(c8, c5) && like(c8, d1) && !like(c7, d2) && !like(c8, d0)) {
  91.             e11 = mix((c8+c5+d1)/3.0, c7, 0.75);
  92.         } else if (like(c7, c4) && like(c7, d2) && !like(c7,d6), !like(c8, d1)) {
  93.             e11 = mix((c7+c4+d2)/3.0, c8, 0.75);
  94.         } else {
  95.             e11 = mix(c7, c8, 0.5);
  96.         }

  97.         if (like(c5, c8) && like(c5, c1) && !like(c5, c0) && !like(c4, c2)) {
  98.             e01 = mix((c5+c8+c1)/3.0, c4, 0.75);
  99.         } else if (like(c4, c7) && like(c4, c2) && !like(c5, c1) && !like(c4, d3)) {
  100.             e01 = mix((c4+c7+c2)/3.0, c5, 0.75);
  101.         } else {
  102.             e01 = mix(c4, c5, 0.5);
  103.         }
  104.     }

  105.     // 处理 e10
  106.     if (like(c4, c8) && like(c4, c3) && !like(c7, c5) && !like(c4, d2)) {
  107.         e10 = mix(c7, (c4+c8+c3)/3.0, 0.5);
  108.     } else if (like(c4, c6) && like(c4, c5) && !like(c7, c3) && !like(c4 ,d0)) {
  109.         e10 = mix(c7, (c4+c6+c5)/3.0, 0.5);
  110.     } else {
  111.         e10 = c7;
  112.     }

  113.     // 处理 e00
  114.     if (like(c7, c5) && like(c7, c6) && !like(c4, c8) && !like(c7, c2)) {
  115.         e00 = mix((c7+c5+c6)/3.0, c4, 0.5);
  116.     } else if (like(c7, c3) && like(c7, c8) && !like(c4, c6) && !like(c7, c0)) {
  117.         e00 = mix((c7+c3+c8)/3.0, c4, 0.5);
  118.     }

  119.     // 混和结果
  120.     vec2 pos = floor(fract(uv * u_tex0Resolution) * 2.0);
  121.     vec4 color = mix(
  122.                 mix(e00, e01, pos.x),
  123.                 mix(e10, e11, pos.x),
  124.                 pos.y);

  125.     gl_FragColor = color;
  126. }
复制代码

注:此为源代码转写,没有优化性能,比如 YUV 转换这里很明显多算了很多次
Moonstruck Blossom
个人网站:dasasdhba.github.io

36

主题

720

回帖

13

精华

版主

经验
7350
硬币
1157 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2023-3-2 11:29:21 | 显示全部楼层
[插曲 4]

那么图像处理类 Shader 暂时告一段落,此类 Shader 更多地与图形学内容相关,故也是一个大坑。
在此推荐一个开源的图像处理相关算法库(Java):http://www.jhlabs.com/index.html
可能会有一定的参考价值。

接下来我们将进入下一个话题:模拟(Simulation)
遗憾的是,该话题仍然没有成体系的资料,故更新速度估计会跟图像处理类一样慢(
Moonstruck Blossom
个人网站:dasasdhba.github.io
您需要登录后才可以回帖 登录 | 创建账户

本版积分规则