Shadertoy学习 vol. 4 菲涅尔, 折射与吸收, 环绕相机

result

前排提醒,这次的内容会比较多,同时会对之前的代码做出大量修改。另外,这是光追渲染器的最后一篇了,下次估计开坑ray marching。

菲涅尔


简单来说,菲涅尔效应是物体在不同观察角度下,表面反射比率不同的现象。具体的效果取决于物体本身的物理特性。模拟菲涅尔可以增强物体材质的真实感。

下面这个函数实现了菲涅尔效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
float FresnelReflectAmount(float n1, float n2, vec3 normal, vec3 incident, float f0, float f90)
{
// Schlick aproximation
float r0 = (n1-n2) / (n1+n2);
r0 *= r0;
float cosX = -dot(normal, incident);
if (n1 > n2)
{
float n = n1/n2;
float sinT2 = n*n*(1.0-cosX*cosX);
// Total internal reflection
if (sinT2 > 1.0)
return f90;
cosX = sqrt(1.0-sinT2);
}
float x = 1.0-cosX;
float ret = r0+(1.0-r0)*x*x*x*x*x;

// adjust reflect multiplier for object reflectivity
return mix(f0, f90, ret);
}

这里n1是入射光线材质的折射率(IOR), n2是被击中的对象材质的折射率,normal是光线碰撞处的表面法线, incident是光线集中对象时的方向, f0是对象的最小反射率(当光线与法线呈0°时), f90是对象的最大反射率(当光线与法线呈90°时).

当应用到我们的渲染器中时,找到这段代码:

1
2
//Wether or not to do the specular reflection ray
float doSpecular = (RandomFloat01(rngState) < hitInfo.material.percentSpecular) ? 1.0f : 0.0f;

改为:

1
2
3
4
5
6
7
8
9
10
11
12
// apply fresnel
float specularChance = hitInfo.material.percentSpecular;
if (specularChance > 0.0f)
{
specularChance = FresnelReflectAmount(
1.0,
hitInfo.material.IOR,
rayDir, hitInfo.normal, hitInfo.material.percentSpecular, 1.0f);
}

// calculate whether we are going to do a diffuse or specular reflection ray
float doSpecular = (RandomFloat01(rngState) < specularChance) ? 1.0f : 0.0f;

注意还要向SMaterialInfo添加IOR

大家可以根据在网上找到的物体IOR来为你的渲染添加更真实的材质:
data

这是菲涅尔效果的演示,从左到右IOR从1增加到2:
test1

折射和吸收

让我们先向SMaterialInfo添加更多的属性,找到结构体,改为:

1
2
3
4
5
6
7
8
9
10
11
12
struct SMaterialInfo
{
vec3 albedo;
vec3 emissive;
vec3 specularColor;
float specularChance;
float specularRoughness;
float IOR;
float refractionChance;
float refractionRoughness;
vec3 refractionColor;
};

现在我们有了镜面反射概率,折射概率,以及一个漫反射概率。漫反射概率为1.0 - specularChance - refractionChance, 并没有出现在结构体中,因为我们默认的光线反射方式就是漫反射。

因为我们的SmaterialInfo有了很多属性,很有可能忘记初始化其中一个造成问题,所以要来写一个初始化函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SMaterialInfo GetZeroedMaterial()
{
SMaterialInfo ret;
ret.albedo = vec3(0.0f, 0.0f, 0.0f);
ret.emissive = vec3(0.0f, 0.0f, 0.0f);
ret.specularChance = 0.0f;
ret.specularRoughness = 0.0f;
ret.specularColor = vec3(0.0f, 0.0f, 0.0f);
ret.IOR = 1.0f;
ret.refractionChance = 0.0f;
ret.refractionRoughness = 0.0f;
ret.refractionColor = vec3(0.0f, 0.0f, 0.0f);
return ret;
}

之后每次新建了一个Material都可以调用这个函数来初始化.

接着向SRayHitInfo添加一个新的属性,叫fromInside

1
2
3
4
5
6
7
struct SRayHitInfo
{
float dist;
vec3 normal;
bool fromInside;
SMaterialInfo material;
};

因为我们现在有透明物体, 所以光线会从物体内部碰到表面. 我们需要知道碰撞究竟是在内部发生的还是在外部发生的. 当然, 我们的长方形是没有内部的,所以在TestQuadTrace里把fromInside直接设成false就可以. 不过在TestSphereTrace里还是要修改这个fromInside的值的, 之前代码里已经有判断的逻辑了, 只要再给它赋个值就可以了.

这里用到比尔-朗伯定律来实现的光线衰减, 给光线乘上系数:\(Multiplier=e^{\left( -absorb \cdot distance \right) }\)

之后对于逻辑的修改内容比较多, 大家可以直接参考代码来理解, 如有不理解的地方欢迎在评论区留言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
vec3 GetColorForRay(in vec3 startRayPos, in vec3 startRayDir, inout uint rngState)
{
vec3 ret = vec3(0.0f, 0.0f, 0.0f);
vec3 throughput = vec3(1.0f, 1.0f, 1.0f);
vec3 rayPos = startRayPos;
vec3 rayDir = startRayDir;

for (int bounceIndex = 0; bounceIndex <= c_numBounces; ++bounceIndex)
{
SRayHitInfo hitInfo;
hitInfo.material = GetZeroedMaterial();
hitInfo.dist = c_superFar;
hitInfo.fromInside = false;
TestSceneTrace(rayPos, rayDir, hitInfo);

if (hitInfo.dist == c_superFar)
{
ret = vec3(0.01f, 0.01f, 0.01f);
break;
}

if (hitInfo.fromInside)
throughput *= exp(-hitInfo.material.refractionColor * hitInfo.dist);

float specularChance = hitInfo.material.specularChance;
float refractionChance = hitInfo.material.refractionChance;


float rayProbability = 1.0f;
if (specularChance > 0.0f)
{
specularChance = FresnelReflectAmount
(
hitInfo.fromInside ? hitInfo.material.IOR : 1.0,
!hitInfo.fromInside ? hitInfo.material.IOR : 1.0,
rayDir,
hitInfo.normal,
hitInfo.material.specularChance,
1.0f
);

float chanceMultiplier = (1.0f - specularChance) / (1.0f - hitInfo.material.specularChance);
refractionChance *= chanceMultiplier;
}

float doSpecular = 0.0f;
float doRefraction = 0.0f;
float raySelectionRoll = RandomFloat01(rngState);
//Wether or not to do the specular reflection ray
if (specularChance > 0.0f && raySelectionRoll < specularChance)
{
doSpecular = 1.0f;
rayProbability = specularChance;
}
else if (refractionChance > 0.0f && raySelectionRoll < specularChance + refractionChance)
{
doRefraction = 1.0f;
rayProbability = refractionChance;
}
else
{
rayProbability = 1.0f - (specularChance + refractionChance);
}

rayProbability = max(rayProbability, 0.001f);

if (doRefraction == 1.0f)
rayPos = (rayPos + rayDir * hitInfo.dist) - hitInfo.normal * c_rayPosNormalNudge;
else
rayPos = (rayPos + rayDir * hitInfo.dist) + hitInfo.normal * c_rayPosNormalNudge;

vec3 diffuseRayDir = normalize(hitInfo.normal + RandomUnitVector(rngState));

vec3 specularRayDir = reflect(rayDir, hitInfo.normal);
specularRayDir = normalize(mix(specularRayDir, diffuseRayDir, hitInfo.material.specularRoughness * hitInfo.material.specularRoughness));

vec3 refractionRayDir = refract(rayDir, hitInfo.normal, hitInfo.fromInside ? hitInfo.material.IOR : 1.0f / hitInfo.material.IOR);
refractionRayDir = normalize(mix(refractionRayDir, normalize(-hitInfo.normal + RandomUnitVector(rngState)), hitInfo.material.refractionRoughness * hitInfo.material.refractionRoughness));

rayDir = mix(diffuseRayDir, specularRayDir, doSpecular);
rayDir = mix(rayDir, refractionRayDir, doRefraction);

ret += hitInfo.material.emissive * throughput;

if (doRefraction == 0.0f)
throughput *= mix(hitInfo.material.albedo, hitInfo.material.specularColor, doSpecular);

throughput /= rayProbability;

{
float p = max(throughput.r, max(throughput.g, throughput.b));
if (RandomFloat01(rngState) > p) break;
throughput *= 1.0f / p;
}
}
return ret;
}

比较值得提及的是在光线刚碰到物体时, 由于我们不知道光线会传播多远, 所以不能立刻计算出吸收的系数. 我们需要等到光线的下一次碰撞, 才能计算出吸收系数.

环绕相机

让我们来我们添加一个鼠标移动相机的功能, 把以下代码添加到main()上方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const float c_minCameraAngle = 0.01f;
const float c_maxCameraAngle = (c_pi - 0.01f);
const vec3 c_cameraAt = vec3(0.0f, 0.0f, 0.0f);
const float c_cameraDistance = 20.0f;

void GetCameraVectors(out vec3 cameraPos, out vec3 cameraFwd, out vec3 cameraUp, out vec3 cameraRight)
{
vec2 mouse = iMouse.xy;
if (dot(mouse, vec2(1.0f, 1.0f)) == 0.0f)
{
cameraPos = vec3(0.0f, 0.0f, -c_cameraDistance);
cameraFwd = vec3(0.0f, 0.0f, 1.0f);
cameraUp = vec3(0.0f, 1.0f, 0.0f);
cameraRight = vec3(1.0f, 0.0f, 0.0f);
return;
}

float angleX = -mouse.x * 16.0f / float(iResolution.x);
float angleY = mix(c_minCameraAngle, c_maxCameraAngle, mouse.y / float(iResolution));

cameraPos.x = sin(angleX) * sin(angleY) * c_cameraDistance;
cameraPos.y = -cos(angleY) * c_cameraDistance;
cameraPos.z = cos(angleX) * sin(angleY) * c_cameraDistance;

cameraPos += c_cameraAt;

cameraFwd = normalize(c_cameraAt - cameraPos);
cameraRight = normalize(cross(vec3(0.0f, 1.0f, 0.0f), cameraFwd));
cameraUp = normalize(cross(cameraFwd, cameraRight));
}

再修改一下主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void main()
{
uint rngState = uint(uint(gl_FragCoord.x) * uint(1973) + uint(gl_FragCoord.y) * uint(9277) + uint(iFrame) * uint(26699)) | uint(1);

vec3 cameraPos, cameraFwd, cameraUp, cameraRight;
GetCameraVectors(cameraPos, cameraFwd, cameraUp, cameraRight);

vec2 jitter = vec2(RandomFloat01(rngState), RandomFloat01(rngState)) - 0.5f;


vec3 rayDir;
{
vec2 uvJittered = (gl_FragCoord.xy + jitter) / iResolution.xy;
vec2 screen = uvJittered * 2.0f - 1.0f;

float aspectRatio = iResolution.x / iResolution.y;
screen.y /= aspectRatio;

float cameraDistance = tan(c_FOVDegrees * 0.5f * c_pi / 180.0f);
rayDir = vec3(screen, cameraDistance);
rayDir = normalize(mat3(cameraRight, cameraUp, cameraFwd) * rayDir);
}

vec3 color = vec3(0.0f, 0.0f, 0.0f);
for (int i = 0; i < c_numRendersPerFrame; ++i)
color += GetColorForRay(cameraPos, rayDir, rngState) / float(c_numRendersPerFrame);

vec4 lastFrameColor = texture(iChannel0, gl_FragCoord.xy / iResolution.xy);

float blend = (iFrame < 2 || iMouse.z > 0.0 || lastFrameColor.a == 0.0f || isKeyPressed(32)) ? 1.0f : 1.0f / (1.0f + (1.0f / lastFrameColor.a));
color = mix(lastFrameColor.rgb, color, blend);

gl_FragColor = vec4(color, blend);
}

好的, 到此就大功告成了!!! (我实在写不动了…)

BTW, 没想到Bandcamp也有Embed功能, 来试试吧

Comments