前排提醒,这次的内容会比较多,同时会对之前的代码做出大量修改。另外,这是光追渲染器的最后一篇了,下次估计开坑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){ 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); 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; return mix (f0, f90, ret); }
这里n1
是入射光线材质的折射率(IOR), n2
是被击中的对象材质的折射率,normal
是光线碰撞处的表面法线, incident
是光线集中对象时的方向, f0
是对象的最小反射率(当光线与法线呈0°时), f90
是对象的最大反射率(当光线与法线呈90°时).
当应用到我们的渲染器中时,找到这段代码:
1 2 float doSpecular = (RandomFloat01(rngState) < hitInfo.material.percentSpecular) ? 1.0 f : 0.0 f;
改为:
1 2 3 4 5 6 7 8 9 10 11 12 float specularChance = hitInfo.material.percentSpecular;if (specularChance > 0.0 f){ specularChance = FresnelReflectAmount( 1.0 , hitInfo.material.IOR, rayDir, hitInfo.normal, hitInfo.material.percentSpecular, 1.0 f); } float doSpecular = (RandomFloat01(rngState) < specularChance) ? 1.0 f : 0.0 f;
注意还要向SMaterialInfo
添加IOR
大家可以根据在网上找到的物体IOR来为你的渲染添加更真实的材质:
这是菲涅尔效果的演示,从左到右IOR从1增加到2:
折射和吸收 让我们先向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.0 f, 0.0 f, 0.0 f); ret.emissive = vec3 (0.0 f, 0.0 f, 0.0 f); ret.specularChance = 0.0 f; ret.specularRoughness = 0.0 f; ret.specularColor = vec3 (0.0 f, 0.0 f, 0.0 f); ret.IOR = 1.0 f; ret.refractionChance = 0.0 f; ret.refractionRoughness = 0.0 f; ret.refractionColor = vec3 (0.0 f, 0.0 f, 0.0 f); 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.0 f, 0.0 f, 0.0 f); vec3 throughput = vec3 (1.0 f, 1.0 f, 1.0 f); 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.01 f, 0.01 f, 0.01 f); break ; } if (hitInfo.fromInside) throughput *= exp (-hitInfo.material.refractionColor * hitInfo.dist); float specularChance = hitInfo.material.specularChance; float refractionChance = hitInfo.material.refractionChance; float rayProbability = 1.0 f; if (specularChance > 0.0 f) { specularChance = FresnelReflectAmount ( hitInfo.fromInside ? hitInfo.material.IOR : 1.0 , !hitInfo.fromInside ? hitInfo.material.IOR : 1.0 , rayDir, hitInfo.normal, hitInfo.material.specularChance, 1.0 f ); float chanceMultiplier = (1.0 f - specularChance) / (1.0 f - hitInfo.material.specularChance); refractionChance *= chanceMultiplier; } float doSpecular = 0.0 f; float doRefraction = 0.0 f; float raySelectionRoll = RandomFloat01(rngState); if (specularChance > 0.0 f && raySelectionRoll < specularChance) { doSpecular = 1.0 f; rayProbability = specularChance; } else if (refractionChance > 0.0 f && raySelectionRoll < specularChance + refractionChance) { doRefraction = 1.0 f; rayProbability = refractionChance; } else { rayProbability = 1.0 f - (specularChance + refractionChance); } rayProbability = max (rayProbability, 0.001 f); if (doRefraction == 1.0 f) 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.0 f / 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.0 f) 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.0 f / 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.01 f;const float c_maxCameraAngle = (c_pi - 0.01 f);const vec3 c_cameraAt = vec3 (0.0 f, 0.0 f, 0.0 f);const float c_cameraDistance = 20.0 f;void GetCameraVectors(out vec3 cameraPos, out vec3 cameraFwd, out vec3 cameraUp, out vec3 cameraRight){ vec2 mouse = iMouse.xy; if (dot (mouse, vec2 (1.0 f, 1.0 f)) == 0.0 f) { cameraPos = vec3 (0.0 f, 0.0 f, -c_cameraDistance); cameraFwd = vec3 (0.0 f, 0.0 f, 1.0 f); cameraUp = vec3 (0.0 f, 1.0 f, 0.0 f); cameraRight = vec3 (1.0 f, 0.0 f, 0.0 f); return ; } float angleX = -mouse.x * 16.0 f / 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.0 f, 1.0 f, 0.0 f), 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.5 f; vec3 rayDir; { vec2 uvJittered = (gl_FragCoord .xy + jitter) / iResolution.xy; vec2 screen = uvJittered * 2.0 f - 1.0 f; float aspectRatio = iResolution.x / iResolution.y; screen.y /= aspectRatio; float cameraDistance = tan (c_FOVDegrees * 0.5 f * c_pi / 180.0 f); rayDir = vec3 (screen, cameraDistance); rayDir = normalize (mat3 (cameraRight, cameraUp, cameraFwd) * rayDir); } vec3 color = vec3 (0.0 f, 0.0 f, 0.0 f); 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.0 f || isKeyPressed(32 )) ? 1.0 f : 1.0 f / (1.0 f + (1.0 f / lastFrameColor.a)); color = mix (lastFrameColor.rgb, color, blend); gl_FragColor = vec4 (color, blend); }
好的, 到此就大功告成了!!! (我实在写不动了…)
BTW, 没想到Bandcamp也有Embed功能, 来试试吧