回顾上一篇 ShaderToy入门教程(1) - SDF 和 Raymarching 算法
继续上篇,这篇涵盖以下黑体所示内容
- 符号距离函数
- Ray-marching算法
- 曲面法线和光照
- 相机变换
- 构造实体形状(CSG)
- 模型变换
- 平移和旋转
- 比例缩放
- 非均匀缩放
- 结论
- 参考
曲面法线和光照
计算机图形中的大多数光照模型使用曲面法线的概念来计算材料在表面上的给定点处应该是什么颜色。 当曲面由显式几何体(如多边形)定义时,通常会为每个顶点指定法线,在表面上的给定点的法线通过它周围的顶点法线通过插值计算获得。
那么我们如何找到由有符号距离函数定义的场景曲面法线? 利用梯度! 从概念上讲,函数
f
f
f在
(
x
,
y
,
z
)
(x,y,z)
(x,y,z)处的梯度告诉你从
(
x
,
y
,
z
)
(x,y,z)
(x,y,z)向哪个方向移动将最快增加
f
f
f的值。
这是直觉:对于表面上的一个点,
f
f
f(我们的SDF),评估为零。在该表面的内侧,
f
f
f变为负,而在外侧,它变为正。 因此,表面上最快速地从负到正的方向将与表面正交。
梯度公式:
∇
f
=
(
∂
f
∂
x
,
∂
f
∂
y
,
∂
f
∂
z
)
\nabla f = \left( \frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}, \frac{\partial f}{\partial z} \right)
∇f=(∂x∂f,∂y∂f,∂z∂f)
但是没有必要使用微积分。 我们不是采用函数的实数导数,而是通过对表面上的点周围的采样点进行近似,就像在学习如何做之前学习如何计算函数中的斜率作为过度一样。
vec3 estimateNormal(vec3 p) {
return normalize(vec3(
sceneSDF(vec3(p.x + EPSILON, p.y, p.z)) - sceneSDF(vec3(p.x - EPSILON, p.y, p.z)),
sceneSDF(vec3(p.x, p.y + EPSILON, p.z)) - sceneSDF(vec3(p.x, p.y - EPSILON, p.z)),
sceneSDF(vec3(p.x, p.y, p.z + EPSILON)) - sceneSDF(vec3(p.x, p.y, p.z - EPSILON))
));
}
有了这些知识,我们可以计算表面上任何一点的法线,在Phong反射模型中使用两个光源,我们得到这个结果:
完整代码如下:
// 这一部分略去, 拷贝上面part1的代码即可
/**
* 对于那些SDF求出来在曲面上的点求标准化的法线向量
*/
vec3 estimateNormal(vec3 p) {
return normalize(vec3(
sceneSDF(vec3(p.x + EPSILON, p.y, p.z)) - sceneSDF(vec3(p.x - EPSILON, p.y, p.z)),
sceneSDF(vec3(p.x, p.y + EPSILON, p.z)) - sceneSDF(vec3(p.x, p.y - EPSILON, p.z)),
sceneSDF(vec3(p.x, p.y, p.z + EPSILON)) - sceneSDF(vec3(p.x, p.y, p.z - EPSILON))
));
}
/**
* Lighting contribution of a single point light source via Phong illumination.
*
* The vec3 returned is the RGB color of the light's contribution.
*
* k_a: 环境色
* k_d: 漫反射颜色
* k_s: 镜面颜色
* alpha: 光泽系数
* p: position of point being lit
* eye: 相机的位置
* lightPos: 光的位置
* lightIntensity: 光的颜色/强度
*
* See https://en.wikipedia.org/wiki/Phong_reflection_model#Description
*/
vec3 phongContribForLight(vec3 k_d, vec3 k_s, float alpha, vec3 p, vec3 eye, vec3 lightPos, vec3 lightIntensity) {
vec3 N = estimateNormal(p);
vec3 L = normalize(lightPos - p);
vec3 V = normalize(eye - p);
vec3 R = normalize(reflect(-L, N));
float dotLN = dot(L, N);
float dotRV = dot(R, V);
if (dotLN < 0.0) {
// Light not visible from this point on the surface
return vec3(0.0, 0.0, 0.0);
}
if (dotRV < 0.0) {
// Light reflection in opposite direction as viewer, apply only diffuse
// component
return lightIntensity * (k_d * dotLN);
}
return lightIntensity * (k_d * dotLN + k_s * pow(dotRV, alpha));
}
/**
* Lighting via Phong illumination.
*
* The vec3 returned is the RGB color of that point after lighting is applied.
* k_a: 环境色
* k_d: 漫反射颜色
* k_s: 镜面颜色
* alpha: 光泽系数
* p: position of point being lit
* eye: 相机的位置
*
* See https://en.wikipedia.org/wiki/Phong_reflection_model#Description
*/
vec3 phongIllumination(vec3 k_a, vec3 k_d, vec3 k_s, float alpha, vec3 p, vec3 eye) {
const vec3 ambientLight = 0.5 * vec3(1.0, 1.0, 1.0);
vec3 color = ambientLight * k_a;
vec3 light1Pos = vec3(4.0 * sin(iTime),2.0, 4.0 * cos(iTime));
vec3 light1Intensity = vec3(0.4, 0.4, 0.4);
color += phongContribForLight(k_d, k_s, alpha, p, eye, light1Pos, light1Intensity);
vec3 light2Pos = vec3(2.0 * sin(0.37 * iTime),
2.0 * cos(0.37 * iTime),
2.0);
vec3 light2Intensity = vec3(0.4, 0.4, 0.4);
color += phongContribForLight(k_d, k_s, alpha, p, eye,
light2Pos,
light2Intensity);
return color;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec3 dir = rayDirection(45.0, iResolution.xy, fragCoord);
vec3 eye = vec3(0.0, 0.0, 5.0);
float dist = shortestDistanceToSurface(eye, dir, MIN_DIST, MAX_DIST);
if (dist > MAX_DIST - EPSILON) {
// Didn't hit anything
fragColor = vec4(0.0, 0.0, 0.0, 0.0);
return;
}
// The closest point on the surface to the eyepoint along the view ray
vec3 p = eye + dist * dir;
vec3 K_a = vec3(0.2, 0.2, 0.2);
vec3 K_d = vec3(0.7, 0.2, 0.2);
vec3 K_s = vec3(1.0, 1.0, 1.0);
float shininess = 10.0;
vec3 color = phongIllumination(K_a, K_d, K_s, shininess, p, eye);
fragColor = vec4(color, 1.0);
}
相机变换
我不会花太多时间,因为这种解决方案并不是Ray marching行进所独有的。 正如在光线跟踪(ray tracing)中一样,要在相机上进行变换,您可以通过变换矩阵来改变视线以定位和旋转相机。
但是,根据一系列的平移和旋转来确定如何定位相机不是那么直观,不是那么实用。 一种更好的描述方法是“我希望相机在这一点上,看相另一点。这正是gluLookAt在OpenGL中的用途。
在着色器内部,我们无法使用该功能,但是我们可以通过gluLookAt使用页的描述,看看它如何计算自己的转换矩阵,然后在GLSL中创建自己的矩阵。
mat4 viewMatrix(vec3 eye, vec3 center, vec3 up) {
vec3 f = normalize(center - eye);
vec3 s = normalize(cross(f, up));
vec3 u = cross(s, f);
return mat4(
vec4(s, 0.0),
vec4(u, 0.0),
vec4(-f, 0.0),
vec4(0.0, 0.0, 0.0, 1)
);
}
由于球的各个角度看起来都一样,因此我在这里把它换成了一个立方体。 将相机放置在(8,5,7)处,并使用新的viewMatrix函数将其指向原点,现在我们有了:
代码在这里
继续阅读下一篇