Skip to content

Instantly share code, notes, and snippets.

@WangNianyi2001
Last active August 15, 2022 15:29
Show Gist options
  • Save WangNianyi2001/f42eef5229b33250f6a455effa094952 to your computer and use it in GitHub Desktop.
Save WangNianyi2001/f42eef5229b33250f6a455effa094952 to your computer and use it in GitHub Desktop.
频谱重映射片元着色器
Shader "Custom/SpectrumRemapping" {
Properties {
_MainTex("Texture", 2D) = "white" {}
_Frequencies("Frequencies (log)", Vector) = (0, 1, 0, 1)
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vertex
#pragma fragment fragment
#include "UnityCG.cginc"
struct VertexIn {
float4 position : POSITION;
float2 uvCoord : TEXCOORD0;
};
struct FragmentIn
{
float2 uvCoord : TEXCOORD0;
float4 position : SV_POSITION;
};
FragmentIn vertex(VertexIn v) {
FragmentIn o;
o.position = UnityObjectToClipPos(v.position);
o.uvCoord = v.uvCoord;
return o;
}
static const fixed pi = atan(1) * 4;
static const fixed tau = atan(1) * 8;
static const fixed sqrt3 = sqrt(3);
static const fixed halfSqrt3 = sqrt3 * .5f;
static const fixed fiveSixth = 5.f / 6;
fixed3 RGB2HSV(fixed3 rgb) {
fixed3 xyz
= rgb.r * fixed3(1, 0, 1)
+ rgb.g * fixed3(-.5f, halfSqrt3, 1)
+ rgb.b * fixed3(-.5f, -halfSqrt3, 1);
fixed h = frac(atan2(xyz.y, xyz.x) / tau + 1);
fixed s = length(xyz.xy);
fixed v = xyz.z / 3;
return fixed3(h, s, v);
}
// HSV: Hue - Spectrum - Value
// XSV: Relative Frequency - # - #
fixed3 HSV2XSV(fixed3 hsv) {
if(hsv.x > fiveSixth)
hsv.x -= 1;
hsv.x /= fiveSixth;
return hsv;
}
fixed3 RGB2XSV(fixed3 rgb) {
return HSV2XSV(RGB2HSV(rgb));
}
fixed3 XSV2HSV(fixed3 xsv) {
fixed offset = abs(xsv.x - clamp(xsv.x, 0, 1));
fixed dim = exp(-offset);
fixed3 hsv = fixed3(
clamp(xsv.x, 0, 1) * fiveSixth,
xsv.y,
xsv.z * dim
);
return hsv;
}
fixed3 HSV2RGB(fixed3 hsv) {
hsv.x *= tau;
fixed3 xyz = fixed3(
fixed2(cos(hsv.x), sin(hsv.x)) * hsv.y,
hsv.z * 3
);
fixed3 rgb
= xyz.x * fixed3(2, -1, -1)
+ xyz.y * fixed3(0, sqrt3, -sqrt3)
+ xyz.z * fixed3(1, 1, 1);
rgb /= 3;
return rgb;
}
fixed3 XSV2RGB(fixed3 xsv) {
return HSV2RGB(XSV2HSV(xsv));
}
float IntervalTransform(float x, float2 source, float2 spectating) {
x = (source.y - source.x) * x + source.x;
x = (x - spectating.x) / (spectating.y - spectating.x);
return x;
}
sampler2D _MainTex;
float4 _Frequencies;
fixed4 fragment(FragmentIn i) : SV_Target{
fixed4 sample = tex2D(_MainTex, i.uvCoord);
fixed3 xsv = RGB2XSV(sample.rgb);
xsv.x = IntervalTransform(xsv.x, _Frequencies.xy, _Frequencies.zw);
fixed3 _rgb = XSV2RGB(xsv);
return fixed4(_rgb, sample.a);
}
ENDCG
}
}
}

需求

实现一个片元着色器,模拟天文望远镜的频谱映射效果。 例如,可见光的频率范围约为 43b-75bHz,小端为红光,大端为蓝(紫)光,超出的部分为不可见光。 那么,现在欲观察 1k-2kHz 的范围,着色器就会把 1kHz 的片元映射为红色,把 2kHz 的片元映射为蓝(紫)色,超出的部分直接变黑。

思路

颜色的本质就是频率嘛。 把源颜色转码成一个片元本身的“频率”,然后对频率做上面的变换,再转换回颜色就好了。 但是这里有两个问题:

  1. 显示器能显示的颜色总共就可见光这么多,现在要表示更广阔的频谱范围,如何表示?
  2. 上面的变换的数学形式何如?

问题二比较好回答,比如现在观察的频谱范围是 $[x,y]$,我们希望将其规约到可见光范围 $[a,b]$ 中去,只需要过一个线性变换即可。 不过这样做可能会导致有负数出现,所以宁可套一层对数,把 $[\ln x,\ln y]$ 变换到 $[\ln a,\ln b]$ 上,然后再指数变换回去即可 (事实上,指数不是必须的,因为频率只是一个中间值,并不对用户可见,我们只要自己清楚参与运算的是对数即可)。

前置知识

对于问题一,必须要知道的一点是:单色光的色相与显示器显示的色相虽然有所重合,但并不是一回事。

人眼的三种视锥细胞会对同一种(复色)光产生三种不同程度的激活,组合而成一个三维向量。 这个向量的“方向”就是人眼感知到的色相,因为形状上是一个圈,所以经常被画成色轮的样子。 但是,单色光能够形成的色相并不完全覆盖整个色轮,粉红色到紫色的这一部分是缺失的。 我们看到的粉色和紫色都是同时包含红色和蓝色波段的复色光所成的。

知道了这个之后,我们可以对一个图片定义一个「源频谱范围」,就像对地形高度图做的那样——红和蓝并不表示实际上的红和蓝,只是在源范围里的最小和最大值罢了。 然后,先将源颜色的 RGB 转换为 HSV,对 H 执行 clamp 操作(为了切掉粉紫色的部分,直接置黑也行),再将归约后的 H 套一层上面的变换,最后再将新的 HSV 逆变换回 RGB 即可。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment