Physically Based Rendering From Theory To Implementation
总目录: Physically Based Rendering From Theory To Implementation - 基于物理的渲染从理论到实践第四版原书翻译
文章目录
7.1 Primitive Interface and Geometric Primitives 图元接口和几何图元
Primitive类定义了Primitive接口。它和本节中描述的Primitive实现在文件cpu/ Primitive.h
和cpu/ Primitive.cpp
中定义。
<<Primitive Definition>>=
class Primitive
: public TaggedPointer<SimplePrimitive, GeometricPrimitive,
TransformedPrimitive, AnimatedPrimitive,
BVHAggregate, KdTreeAggregate> {
public:
<<Primitive Interface>>
};
Primitive接口仅由三个方法组成,每个方法对应一个Shape方法。第一个是Bounds(),它返回一个包围盒,将图元的几何图形包含在渲染空间中。这样的包围有很多用途;其中最重要的是将图元放在加速数据结构中。
<<Primitive Interface>>=
Bounds3f Bounds() const;
另外两种方法提供了两种类型的射线相交测试。
<<Primitive Interface>>+=
pstd::optional<ShapeIntersection> Intersect(const Ray &r,
Float tMax = Infinity) const;
bool IntersectP(const Ray &r, Float tMax = Infinity) const;
在找到交集之后,Primitive的Intersect()
方法还负责初始化它返回的ShapeIntersection
中的SurfaceInteraction
中的几个成员变量。前两个表示形状的材料和它的发射特性,如果它本身是一个发射器。为方便起见,SurfaceInteraction提供了一种方法来设置这些属性,从而降低了无意中没有设置所有属性的风险。后两个与介质散射特性有关,初始化它们的片段将在稍后的11.4节中描述。
<<SurfaceInteraction Public Methods>>+=
void SetIntersectionProperties(Material mtl, Light area,
const MediumInterface *primMediumInterface, Medium rayMedium) {
material = mtl;
areaLight = area;
<<Set medium properties at surface intersection>>
}
7.1.1 Geometric Primitives 几何图元
GeometricPrimitive类提供了Primitive接口的基本实现,该接口存储了可能与形状相关联的各种属性。
class GeometricPrimitive {
public:
<<GeometricPrimitive Public Methods>>
private:
<<GeometricPrimitive Private Members>>
};
每个GeometricPrimitive都包含一个Shape及其外观属性的描述,包括它的材料,它的发射属性(如果它是光源),其表面每侧的参与介质,以及可选的alpha纹理,可用于使形状表面的某些部分消失。
<<GeometricPrimitive Private Members>>=
Shape shape;
Material material;
Light areaLight;
MediumInterface mediumInterface;
FloatTexture alpha;
GeometricPrimitive构造函数根据传递给它的参数初始化这些变量。它很简单,所以我们在这里不包括它。
Primitive接口的大多数方法都是从调用相应的Shape方法开始的。例如,它的Bounds()方法直接从Shape返回Bounds()。
<<GeometricPrimitive Method Definitions>>=
Bounds3f GeometricPrimitive::Bounds() const {
return shape.Bounds();
}
GeometricPrimitive::Intersect() 调用其Shape的Intersect()方法来进行实际的相交测试,并初始化一个ShapeIntersection来描述相交(如果有的话)。如果找到交集,则执行特定于GeometricPrimitive的附加处理。
<<GeometricPrimitive Method Definitions>>+=
pstd::optional<ShapeIntersection>
GeometricPrimitive::Intersect(const Ray &r, Float tMax) const {
pstd::optional<ShapeIntersection> si = shape.Intersect(r, tMax);
if (!si) return {};
<<Test intersection against alpha texture, if present>>
<<Initialize SurfaceInteraction after Shape intersection>>
return si;
}
如果alpha纹理与shape相关联,那么在报告成功的交集之前,将针对alpha纹理测试交集点。(纹理接口的定义和一些实现在第10章中。)alpha纹理可以被认为是形状表面上的标量函数,它指示表面是否实际存在于每个点上。alpha值为0表示不存在,1表示存在。Alpha纹理对于表示像叶子这样的物体很有用:叶子可能被建模为单个三角形或双线性斑块,Alpha纹理切割掉边缘,以便保留叶子的详细轮廓。
<<Test intersection against alpha texture, if present>>=
if (alpha) {
if (Float a = alpha.Evaluate(si->intr); a < 1) {
<<Possibly ignore intersection based on stochastic alpha test>>
}
}
如果alpha纹理在交点处的值为0或1,则很容易确定shape的交点是否有效。对于中间alpha值,正确答案是不太清楚的。
一种可能性是使用固定的阈值,例如,接受alpha为1的所有交叉点,否则忽略它们。然而,这种方法会导致最终边界处的硬转换。另一种选择是从交集方法返回alpha值,并让调用代码来处理它,在这些点上有效地将表面视为部分透明。然而,这种方法不仅会使图元交集接口更加复杂,而且会给积分器带来新的负担,要求他们计算这些交点的着色,并跟踪额外的光线以找到它们后面可见的东西。
随机alpha测试可以解决这些问题。有了它,与形状的交点被随机返回,其概率与alpha纹理的值成正比。这种方法很容易实现,在alpha值为0或1的情况下可以得到预期的结果,并且使用足够数量的样本可以得到比使用固定阈值更好的结果。图7.1比较了这些方法。
图7.1: 随机Alpha测试与使用固定阈值的比较。
(a)示例场景:使用带有alpha纹理的单个四边形对两个冷杉树枝进行建模。
(b)如果alpha测试使用固定阈值,则形状不能真实地再现。这里使用的阈值为1,导致收缩和锯齿边缘。
©如果使用随机alpha检验,结果是更平滑和更真实的过渡。
执行随机alpha检验的一个挑战是生成一个统一的随机数来应用它。对于给定的射线和形状,我们希望这个数字在系统的多次运行中是相同的;这样做是使PBRT执行的计算集具有确定性的一部分,这对调试有很大帮助。如果在不同的系统运行中使用了不同的随机数,那么我们可能会在某些运行中遇到运行时错误,而在其他运行中则不会。然而,对不同的射线使用不同的随机数是很重要的;否则,该方法可能会沦为使用固定阈值的方法。
HashFloat()实用函数为这个问题提供了一个解决方案。在这里,它用于计算alpha测试中0到1之间的随机浮点值;这个值是由光线的原点和方向决定的。
<<Possibly ignore intersection based on stochastic alpha test>>=
Float u = (a <= 0) ? 1.f : HashFloat(r.o, r.d);
if (u > a) {
<<Ignore this intersection and trace a new ray>>
}
如果alpha测试表明应该忽略交集,则使用当前的GeometricPrimitive执行另一个交集测试,并递归调用Intersect()。这个额外的测试对于像球体这样的形状很重要,在这种形状中,我们可能会拒绝最近的相交,但然后沿着光线再次相交形状。这个递归调用需要调整传递给它的tMax值,以考虑沿着光线到初始alpha测试交叉点的距离。然后,如果它报告一个交叉点,则报告的tHit值也应该考虑到该段。
<<Ignore this intersection and trace a new ray>>=
Ray rNext = si->intr.SpawnRay(r.d);
pstd::optional<ShapeIntersection> siNext = Intersect(rNext, tMax - si->tHit);
if (siNext)
siNext->tHit += si->tHit;
return siNext;
给定一个有效的交集,GeometricPrimitive可以继续并最终确定SurfaceInteraction对交集的表示。
<<Initialize SurfaceInteraction after Shape intersection>>=
si->intr.SetIntersectionProperties(material, areaLight, &mediumInterface,
r.medium);
IntersectP()方法还必须处理与之关联的具有alpha纹理的GeometricPrimitive的情况。在这种情况下,可能有必要考虑光线与形状的所有交点,以确定是否存在有效的交点。因为在形状中的IntersectP()实现在发现任何交集时就会提前返回,而且因为它们不返回与交集相关的几何信息,所以在本例中执行完整的交集测试。在更常见的没有alpha纹理的情况下,可以直接调用Shape::IntersectP()。
<<GeometricPrimitive Method Definitions>>+=
bool GeometricPrimitive::IntersectP(const Ray &r, Float tMax) const {
if (alpha)
return Intersect(r, tMax).has_value();
else
return shape.IntersectP(r, tMax);
}
场景中的大多数对象既没有发光也没有alpha纹理。此外,其中只有少数对象典型地代表了两种不同类型参与介质之间的边界。在这种常见情况下,为GeometricPrimitive的相应成员变量存储nullptr值是一种浪费。因此,pbrt还提供了SimplePrimitive,它也实现了Primitive接口,但不存储这些值。将解析过的场景表示转换为场景以供渲染的代码在可能的情况下使用SimplePrimitive代替GeometricPrimitive。
<<SimplePrimitive Definition>>=
class SimplePrimitive {
public:
<<SimplePrimitive Public Methods>>
private:
<<SimplePrimitive Private Members>>
};
因为SimplePrimitive只存储形状和材质,所以它节省了32字节的内存。对于具有数百万个图元的场景,总的节省是有意义的。
<<SimplePrimitive Private Members>>=
Shape shape;
Material material;
我们在这里不包括SimplePrimitive实现的其余部分;它实际上是GeometricPrimitive的简化子集。
7.1.2 Object Instancing and Primitives in Motion 运动中的对象实例化和图元
图7.2:这个户外场景大量使用实例作为压缩场景描述的机制。虽然场景中只有2400万个唯一的三角形,但由于通过实例重用对象,总几何复杂性为31亿个三角形。(场景由Laubwerk提供)
对象实例化是一种经典的渲染技术,它在场景中的多个位置重用单个几何集合的转换副本。例如,在拥有数千个相同座位的音乐厅模型中,如果所有座位都引用单个座位的共享几何表示,则可以大大压缩场景描述。在图7.2的生态系统场景中,虽然只有31种独特的植物模式,但有23241种不同类型的植物个体。因为每个植物模型被实例化多次,每个实例都有不同的变换,所以完整的场景总共有31亿个三角形。然而,由于通过对象实例重用图元,只有2400万个三角形存储在内存中。pbrt在使用对象实例渲染场景时仅使用超过4GB的内存(bvh为1.7 GB, primitive为707 MB,三角形网格为877 MB,纹理图像为846 MB),但如果不进行实例化渲染,则需要516 GB以上的内存。
- 先前版本的pbrt在渲染这个场景时使用了7GB的内存,其中大部分差异是由于内存效率较低的Primitive表示,虚拟函数指针与每个Shape和每个Primitive一起存储,以及使用32位浮点数来存储图像纹理像素,甚至对于最初存储为8位值的纹理。
Primitive接口的TransformedPrimitive实现使得对象实例化在pbrt中成为可能。它不是保存形状,而是存储单个Primitive和Transform,Transform被添加到底层图元和它在场景中的表示之间。这个额外的转换支持对象实例化。
回想一下,第6章的Shape本身是从对象空间(局部空间)转换中渲染出来的,从而将它们放置在场景中。如果一个形状是由TransformedPrimitive 保存的,那么这个形状的渲染空间概念就不是实际的场景渲染空间——只有在 TransformedPrimitive 的转换也被应用之后,这个形状才真正在渲染空间中。对于这里的应用程序,让形状完全不知道所应用的附加转换是有意义的。对于实例图元,让shape知道所有实例转换具有有限的实用性:我们不希望TriangleMesh为每个实例转换制作一个顶点位置的副本,并将它们一直转换到渲染空间,因为这将丧失对象实例化的内存节省效果。
<<TransformedPrimitive Definition>>=
class TransformedPrimitive {
public:
<<TransformedPrimitive Public Methods>>
private:
<<TransformedPrimitive Private Members>>
};
TransformedPrimitive构造函数接受一个表示模型和将其放置在场景中的转换的Primitive。如果实例化的几何图形由多个图元描述,则调用代码负责将它们放在聚合(aggregate)中,以便只需要在这里存储一个图元。
<<TransformedPrimitive Public Methods>>=
TransformedPrimitive(Primitive primitive,
const Transform *renderFromPrimitive)
: primitive(primitive), renderFromPrimitive(renderFromPrimitive) { }
<<TransformedPrimitive Private Members>>=
Primitive primitive;
const Transform *renderFromPrimitive;
考虑从图元空间转换到渲染空间的效果,TransformedPrimitive的关键任务是:在它实现的Primitive接口和它持有的Primitive接口之间架起桥梁。如果 primitive 成员有自己的转换,那么应该将其解释为从对象空间到TransformedPrimitive的坐标系统的转换。到渲染空间的完整转换需要这两个转换一起完成。
<<TransformedPrimitive Public Methods>>+=
Bounds3f Bounds() const {
return (*renderFromPrimitive)(primitive.Bounds());
}
Intersect()方法还必须考虑到转换,既包括传递给当前图元的光线,也包括它返回的任何相交信息。
<<TransformedPrimitive Method Definitions>>=
pstd::optional<ShapeIntersection>
TransformedPrimitive::Intersect(const Ray &r, Float tMax) const {
<<Transform ray to primitive-space and intersect with primitive>>
<<Return transformed instance’s intersection information>>
}
该方法首先将给定的光线转换为图元的坐标系,并将转换后的光线传递给它的Intersect()例程。
<<Transform ray to primitive-space and intersect with primitive>>=
Ray ray = renderFromPrimitive->ApplyInverse(r, &tMax);
pstd::optional<ShapeIntersection> si = primitive.Intersect(ray, tMax);
if (!si) return {};
给定一个交点,SurfaceInteraction需要转换为渲染空间;图元的交集方法已经将SurfaceInteraction转换为其渲染空间的概念,所以这里我们只需要应用TransformedPrimitive所持有的额外转换的效果。
注意,任何从图元返回的 ShapeIntersection::tHit 的值都可以原样返回给调用者;回想一下6.1.4节中关于交点坐标空间和射线 t 值的讨论。
<<Return transformed instance’s intersection information>>=
si->intr = (*renderFromPrimitive)(si->intr);
return si;
IntersectP()方法与此类似,因此省略了。
AnimatedPrimitive类使用AnimatedTransform来代替TransformedPrimitives存储的Transform。因此,它支持场景中图元的刚体动画。由于动画转换而显示运动模糊的图像参见 图 fig:spinning-spheres。
<<AnimatedPrimitive Definition>>=
class AnimatedPrimitive {
public:
<<AnimatedPrimitive Public Methods>>
private:
<<AnimatedPrimitive Private Members>>
};
AnimatedTransform类比Transform类使用更多的内存。在用于开发pbrt的系统上,前者使用696字节的内存,后者使用128字节的内存。因此,就像使用GeometricPrimitive和SimplePrimitive的情况一样,只对实际是动画的形状使用AnimatedPrimitive是值得的。做出这种区分是构建用于渲染的场景规范的代码的任务。
<<AnimatedPrimitive Private Members>>=
Primitive primitive;
AnimatedTransform renderFromPrimitive;
通过AnimatedTransform::MotionBounds()方法可以在帧的时间范围内找到图元的包围盒。
<<AnimatedPrimitive Public Methods>>=
Bounds3f Bounds() const {
return renderFromPrimitive.MotionBounds(primitive.Bounds());
}
我们还将跳过AnimatedPrimitive交叉方法的其余实现;它们与TransformedPrimitive类似,只是使用了AnimatedTransform。