现在我们回顾了影响对象外观的参数(它们的亮度、颜色等),我们准备开始研究一些简单的着色技术。
推荐:用 NSDT编辑器 快速搭建可编程3D场景
1、法线
法线在着色中起着核心作用。 大家都知道,如果我们将物体朝向光源,它就会变得更亮。 物体表面的方向对其反射的光量(以及其看起来的亮度)起着重要作用。在物体表面的任一点P的这个方向,可以通过垂直于表面的法线N来表示,如图1所示。
图 1:注意随着法线方向和光线方向之间的角度增加,球体如何变得更暗。
请注意图 1 中,球体的亮度如何随着光线方向与法线方向之间的角度增大而减小。 这种亮度下降是我们每天都能看到的现象,但可能很少有人知道为什么会发生这种情况。 我们稍后将解释这种现象的原因。 现在,只需要记住:
- 我们所说的法线(我们用大写字母N表示) 是垂直于表面P点切线的向量。 换句话说,要找到P点的法线,我们需要追踪一条与曲面相切的线,然后取垂直于该切线的向量(请注意,在 3D 中,这将是切平面)。
- 物体表面上一点的亮度取决于法线方向,法线方向定义了物体表面在该点相对于光的方向。 另一种说法是,物体表面任意给定点的亮度取决于该点法线与光线方向之间的角度。
现在的问题是我们如何计算这个法线? 根据渲染的几何类型,该问题的解决方案的复杂性可能会有很大差异。 球体的法线通常很容易找到。 如果我们知道球体表面上的点的位置和球心,则可以通过球心减去该点的位置来计算该点的法线:
Vec3f N = P - sphereCenter;
如果对象是三角形网格,则每个三角形定义一个平面,并且垂直于该平面的矢量是位于该三角形表面上的任何点的法线。 垂直于三角形平面的向量可以通过该三角形两条边的叉积轻松获得。 请记住 v1xv2 = -v2xv1。 所以边的选择会影响法线的方向。 如果按逆时针顺序声明三角形顶点,则可以使用以下代码:
Vec3f N = (v1-v0).crossProduct(v2-v0);
图 2:三角形的面法线可以通过该三角形两条边的叉积来计算。
如果三角形位于 xz 平面,则所得法线应为 (0,1,0) 而不是 (0,-1,0),如图 2 所示
以这种方式计算法线就得到了我们所说的面法线(因为整个面的法线是相同的,无论你在该面或三角形上选取的点如何)。 三角形网格的法线也可以定义在三角形的顶点处,在这种情况下,我们将这些法线称为顶点法线。 顶点法线用于一种称为平滑着色的技术,你将在本章末尾找到该技术的描述。 目前,我们只处理面法线。
在程序中如何以及何时计算要着色的点处的表面法线并不重要。 重要且重要的是,当你要着色这一点时,手头有这些信息。 在本节中我们做了一些基本着色的几个程序中,我们在每个几何类中实现了一个名为 getSurfaceProperties() 的特殊方法,其中我们计算了交点处的法线(如果使用光线跟踪)和其他变量 例如我们将在本课后面讨论的纹理坐标。 对于球体和三角形网格几何类型,这些方法的实现如下所示:
class Sphere : public Object
{
...
public:
...
void getSurfaceProperties(
const Vec3f &hitPoint,
const Vec3f &viewDirection,
const uint32_t &triIndex,
const Vec2f &uv,
Vec3f &hitNormal,
Vec2f &hitTextureCoordinates) const
{
hitNormal= Phit - center;
hitNormal.normalize();
...
}
...
};
class TriangleMesh : public Object
{
...
public:
void getSurfaceProperties(
const Vec3f &hitPoint,
const Vec3f &viewDirection,
const uint32_t &triIndex,
const Vec2f &uv,
Vec3f &hitNormal,
Vec2f &hitTextureCoordinates) const
{
// face normal
const Vec3f &v0 = P[trisIndex[triIndex * 3]];
const Vec3f &v1 = P[trisIndex[triIndex * 3 + 1]];
const Vec3f &v2 = P[trisIndex[triIndex * 3 + 2]];
hitNormal = (v1 - v0).crossProduct(v2 - v0);
hitNormal.normalize();
...
}
...
};
2、简单的着色效果:面比率
现在我们知道如何计算物体表面上的点的法线,我们已经有足够的信息来创建一个简单的着色效果,称为面比率(facing ratio)。 该技术包括计算我们想要着色的点的法线与观察方向的点积。 计算观察方向也非常简单。 当使用光线追踪时,它只是与表面相交处P的光线的相反方向。 如果不使用光线追踪,也可以通过从表面上的P点追踪一条到眼睛的线来简单地找到观察方向:
Vec3f V = (E - P).normalize(); // or -ray.dir if you use ray-tracing
请记住,如果两个向量平行且指向同一方向,则两个向量的点积返回 1;如果两个向量彼此垂直,则点积返回 0。 如果向量指向相反的方向,则点积为负,但如果我们使用该点积的结果作为颜色,那么我们无论如何都不会对负值感兴趣。 如果需要有关点积的介绍,请查看几何课程。 为了避免负面结果,我们需要将结果限制为 0:
float facingRatio = std::max(0, N.dotProduct(V));
当法线和向量 V 指向同一方向时,点积返回 1。 如果两个向量垂直,则结果为 0。 如果我们使用这种简单的技术对位于框架中间的球体进行着色,那么球体的中心将是白色的,并且当我们远离其中心向边缘移动时,球体将变得更暗,如下所示。
Vec3f castRay(
const Vec3f &orig, const Vec3f &dir,
const std::vector<std::unique_ptr<Object>> &objects,
const Options &options)
{
Vec3f hitColor = options.backgroundColor;
float tnear = kInfinity;
Vec2f uv;
uint32_t index = 0;
Object *hitObject = nullptr;
if (trace(orig, dir, objects, tnear, index, uv, &hitObject)) {
Vec3f hitPoint = orig + dir * tnear; //shaded point
Vec3f hitNormal;
Vec2f hitTexCoordinates;
// compute the normal of the point we want to shade
hitObject->getSurfaceProperties(hitPoint, dir, index, uv, hitNormal, ...);
hitColor = std::max(0.f, hitNormal.dotProduct(-dir)); //facing ratio
}
return hitColor;
}
恭喜! 你刚刚了解了第一种着色技术。 现在让我们了解一种更真实的着色方法,该方法将模拟漫反射对象上的光效果。 但在了解这种方法之前,我们首先需要介绍和了解光的概念。
3、平面着色、平滑着色和顶点法线
三角形网格的问题在于它们无法表示完全光滑的表面(除非三角形非常小)。 如果我们希望将刚刚描述的面向比技术应用于多边形网格,我们需要计算与射线相交的三角形的法线,并将面向比计算为该面法线与视图方向之间的点积 。 这种方法的问题在于,它使对象呈现出多面的外观,如下图所示。 因此这种着色方法被称为平面着色
正如前面课程中多次提到的,通过计算向量 v0v1 和向量 v0v2 的叉积,可以简单地找到三角形的法线,其中 v0、v1 和 v2 代表三角形的顶点。 为了解决这个问题,Henri Gouraud 在 1971 年引入了一种方法,现在称为平滑着色(smooth shading)或 Gouraud 着色。
该技术背后的想法是在多边形网格的表面上产生连续的阴影,即使网格表示的对象并不连续,因为它是由平坦表面的集合(多边形或三角形)构建的。 为此,Gouraud 引入了顶点法线的概念。 这个想法很简单。 我们不是计算或存储面的法线,而是在网格的每个顶点存储法线,其中法线的方向由三角形网格转换自的底层平滑表面确定。 当我们想要计算三角形表面上的点的颜色时,我们可以通过使用命中点重心坐标对在三角形顶点定义的顶点法线进行线性插值来计算“假平滑”法线,而不是使用面法线 。
该技术如上图所示。 顶点法线在三角形的顶点处定义。 你可以看到它们的方向垂直于构建三角形网格的光滑底层表面。 有时,三角形网格不是直接从平滑表面转换而来,并且必须即时计算顶点法线。 当没有光滑表面来计算顶点法线时,计算顶点法线的不同技术,但我们不会在本课中研究它们。 现在,使用 Maya 或 Blender 等软件来为你完成这项工作,在 Maya 中,你可以选择多边形网格并选择“法线”菜单中的“软化边缘”选项。
事实上,从实用和技术的角度来看,每个三角形都有自己的一组 3 个顶点法线。 这意味着三角形网格的顶点法线总数等于三角形数量乘以 3。在某些情况下,在由 2、3 个或更多三角形共享的顶点上定义的顶点法线是相同的(它们指向 相同的方向),但你可以通过为它们提供不同的方向来实现不同的效果。例如,可以在表面上伪造一些硬边缘。
计算三角形表面上任意点的插值法线的源代码很简单,只要我们知道三角形的顶点法线、三角形上该点的重心坐标以及三角形索引即可。 光栅化和光线追踪都可以为你提供此信息。 顶点法线由你用于创建模型的 3D 程序在模型上生成。 然后将它们导出到几何文件,其中包含三角形的连接信息、顶点位置和三角形的纹理坐标。 然后你需要做的就是结合点重心坐标和三角形顶点法线来计算点插值平滑法线(下面第 17-20 行):
void getSurfaceProperties(
const Vec3f &hitPoint,
const Vec3f &viewDirection,
const uint32_t &triIndex,
const Vec2f &uv,
Vec3f &hitNormal,
Vec2f &hitTextureCoordinates) const
{
// face normal
const Vec3f &v0 = P[trisIndex[triIndex * 3]];
const Vec3f &v1 = P[trisIndex[triIndex * 3 + 1]];
const Vec3f &v2 = P[trisIndex[triIndex * 3 + 2]];
hitNormal = (v1 - v0).crossProduct(v2 - v0);
#if 1
// compute "smooth" normal using Gouraud's technique (interpolate vertex normals)
const Vec3f &n0 = N[trisIndex[triIndex * 3]];
const Vec3f &n1 = N[trisIndex[triIndex * 3 + 1]];
const Vec3f &n2 = N[trisIndex[triIndex * 3 + 2]];
hitNormal = (1 - uv.x - uv.y) * n0 + uv.x * n1 + uv.y * n2;
#endif
// doesn't need to be normalized as the N's are normalized but just for safety
hitNormal.normalize();
// texture coordinates
const Vec2f &st0 = texCoordinates[trisIndex[triIndex * 3]];
const Vec2f &st1 = texCoordinates[trisIndex[triIndex * 3 + 1]];
const Vec2f &st2 = texCoordinates[trisIndex[triIndex * 3 + 2]];
hitTextureCoordinates = (1 - uv.x - uv.y) * st0 + uv.x * st1 + uv.y * st2;
}
请注意,这只会产生表面光滑的印象。 如果你查看下图中的多边形球体,仍然可以看到轮廓是多面的,即使内部表面看起来很光滑。 该技术改善了三角形网格的外观,但当然并不能完全解决其多面外观的问题。 该问题的唯一解决方案是使用细分曲面(我们将在不同的部分中讨论),或者当然增加将平滑曲面转换为三角形网格时使用的三角形数量。
我们还没有准备好学习如何重现漫反射表面的外观。 尽管漫射表面需要光线才能可见。 因此,在研究这项技术之前,我们首先需要了解如何处理 3D 引擎中的光源概念。
原文链接:面法线和顶点法线 — BimAnt