美文网首页
pbrt笔记--第十章 纹理

pbrt笔记--第十章 纹理

作者: 奔向火星005 | 来源:发表于2019-11-27 12:02 被阅读0次

写在前面的个人总结

了解OpenGL的同学应该都对纹理Texture非常熟悉,纹理通常是一张2D图片,我们可以很容易的利用OpenGL接口将纹理“贴”到渲染物体的表面,赋予表面丰富多彩的细节。但是OpenGL等图形API隐藏了大量细节,比如纹理是如何映射到渲染物体上,当纹理尺寸与渲染的图像尺寸不一样时,是如何处理缩放的,mipmap是如何实现的等等。本章的内容虽然是属于离线渲染,但对我们理解OpenGL等图形接口的Texture有非常大的启发。因此我觉得本章的内容相等精彩。

我觉得无论是理解OpenGL的texture接口,或是本章的内容,首先最重要的是要理解纹理的三个空间之间的映射:image Space;world Space;texture Space。如下图(图片来自《Fundamentals of Computer Graphics 4th》):

image Space相当于相机模型中的film,或者是最终要渲染的图像(image plane);world Space相当于真实场景中的shape所在的空间;texture Space表示纹理空间。

10.1 Sampling and Antialiasing(采样和抗混淆)

指导性的一堆话

第七章中采样的aliasing问题很令人沮丧,因为几何体的边缘和hard shadows(硬阴影)包含了无穷高的频率成分,无论用多高的采样频率都无法消除。唯一的安慰就是我们可以通过将这些aliasing转换为没那么令人讨厌的随机噪声。

幸运的是,处理textures就没那么艰难了:texture function的形式通常是比较简单的,这样我们就可以在对它进行采样前先过滤掉高频成分,或者在计算时小心处理,以免引入高频信号。当texture implementation问题处理得当后(本章剩余要做的事),每个像素只需要一个采样点就可以渲染出没有aliasing的图像。

为了消除texture functions的aliasing,有两个问题必须处理:
1.必须计算在texture空间中的采样速率。screen空间中的采样速率可以从image分辨率和pixel采样速率得到,但我们需要确定在scene的一个surface上的采样速率,来得到被采样的texture function上的采样速率。
2.得到texture采样速率后,根据采样定理,texture变化的速率必须低于奈比斯特速率。

这两个问题会在本节的剩余部分解决。

10.1.1 Finding the Texture Sampling Rate

假设一个定义在scene中一个表面上某一点的texture function,T(p),(p为真实场景中的坐标,T(p)可以认为从world space映射到texture space)。如果我们忽略scene中的各种遮挡什么乱七八糟的问题,这个texture function也可以表达为在image平面上的一点的函数,T (f (x , y )),(x,y是在image平面上的坐标,f (x , y )是真实场景中的某一点,也就是p,或者说从image space映射到world space,而T (f (x , y ))可以认为是从image space映射到texture space)。

一个简单的例子,假设一个2D texture function T(s, t),要把它映射到世界空间中的一个四边形,这个四边形垂直于z轴,并且它的四个顶点为(0, 0, 0), (1, 0, 0), (1, 1, 0), and (0, 1, 0)。如果我们使用一个观察方向为z轴的正交相机模型,那么这个四边形就正好填满image plane,那么有


那么(s ,t)和屏幕坐标(x, y)的关系为:


其中image分辨率为(xr, yr)。如图10.2。因为在image平面上的采样间隔是一个像素的大小,因此在texture平面的(s, t)方向上的采样间隔是(1/xr, 1/yr),而我们必须过滤掉texture function中高于该采样频率的成分。


由pixel coordinates(也就是image空间)和texture coordinates(texture空间)的关系,可以的得到他们采样率的关系,这也是决定texture function最大采样率的关键

对于更复杂的scene几何体,相机投影以及pixel coordinates到texture coordinates的映射关系,想要精确的求出image positions和texture parameter值的关系是很困难的。但幸运的是,对于texture antialiasing,我们不需要求出任意(x, y)坐标的 f(x, y)(也就是image上的一点对应在scene中的哪个位置p),我们只需计算image上一点,它在 pixel采样位置的变化 和 对应的texture采样位置的变化 的关系。 这个关系可以通过f(x, y)的偏导数,∂f/∂x and ∂f/∂y得到。例如,我们可以通过偏导数求出f附近的一个点,

如果偏导数在x′ − x和y' − y的距离内变化很慢,这个近似是靠谱的。更重要的是,这些偏导数的值给出了,在image平面上的采样点在x和y方向偏移一个像素,对应在texture平面上的采样点的近似变化率,这也是texture采样率。例如,前面的例子,∂s/∂x = 1/xr, ∂s/∂y = 0, ∂t/∂x = 0, and ∂t/∂y = 1/yr。

得到这些偏导数的关键是在RayDifferential结构体(在2.5.1节定义的)。除了包含用于光线追踪的main ray,它还包含有两个额外的ray,相对于camera ray,它们一条是在水平方向偏移了一个pixel像素,一条是在垂直方向偏移了一个像素。(可以看第六章笔记的6.2.2节最后一个图)

SurfaceInteraction中的各种偏导数

现在我们将使用者两个偏移rays来估算,从image space上的一点到world space上一点的映射p(x, y)的偏导数,以及从image space到texture space的映射u(x, y) and v(x, y)的偏导数。p(x, y)的偏导数是∂p/∂x and ∂p/∂y,u(x, y) and v(x, y)的偏导数是∂u/∂x, ∂v/∂x, ∂u/∂y, and ∂v/∂y。这些值都存储在SurfaceInteraction结构体中。

class SurfaceInteraction : public Interaction {
//其他省略了

    mutable Vector3f dpdx, dpdy;
    mutable Float dudx = 0, dvdx = 0, dudy = 0, dvdy = 0;
}
SurfaceInteraction::ComputeDifferentials()

SurfaceInteraction::ComputeDifferentials()接口计算这些偏导数。

计算这些近似值的关键是,假设表面上的点(在world space上的几何图形)相对于该点的采样速率来说是平坦的。在实践中这个近似是靠谱的,很难有比这更好的方法。因为光线追踪是一种point-sampling(以点为单位)的技术,我们没有在两个ray之间的相关信息。对于高度扭曲的表面,这个近似可能是错的,但在实践中一般不容易察觉。

基于这个近似,我们需要一个通过点p的平面(p是main ray和物体表面的交点),该表面和表面相切。这个平面可以用隐式方程来表示:


其中a = nx, b = ny, c = nz,d = -(n * p)。n是表面的法线。然后我们可以从辅助射线rx和ry中计算它们和该平面的交点px和py(如图10.3)。由这些交点可以得到∂p/∂x and ∂p/∂y的近似:

因为这两个辅助rays是对main ray的偏移是一个像素,因此不需要在除以𝚫值,因为𝚫 = 1。

在3.1.2节中,一个原点为o,方向为d的ray与平面ax + by + cz +
d = 0的交点的t值为:

为了计算两个辅助rays的t,首先计算平面的d值。没有比较计算系数a,b,c,因为它们就是发现n。相关代码如下:

void SurfaceInteraction::ComputeDifferentials(
    const RayDifferential &ray) const {
    if (ray.hasDifferentials) {
        // Estimate screen space change in $\pt{}$ and $(u,v)$

        // Compute auxiliary intersection points with plane
        Float d = Dot(n, Vector3f(p.x, p.y, p.z));
        Float tx =
            -(Dot(n, Vector3f(ray.rxOrigin)) - d) / Dot(n, ray.rxDirection);
        if (std::isinf(tx) || std::isnan(tx)) goto fail;
        Point3f px = ray.rxOrigin + tx * ray.rxDirection;
        Float ty =
            -(Dot(n, Vector3f(ray.ryOrigin)) - d) / Dot(n, ray.ryDirection);
        if (std::isinf(ty) || std::isnan(ty)) goto fail;
        Point3f py = ray.ryOrigin + ty * ray.ryDirection;
        dpdx = px - p;
        dpdy = py - p;

// Compute $(u,v)$ offsets at auxiliary points
// Choose two dimensions to use for ray offset computation
}

有了两个辅助点px和py之后,我们还要求出它们在texture space上的对应的点的坐标是什么。首先,我们用偏导数∂p/∂u和∂p/∂v在平面上建立一个坐标系(注意是∂p/∂u和∂p/∂v,而不是上面的∂p/∂x和∂p/∂y,∂p/∂u和∂p/∂v是表示world space到texture space的映射,可以看第3章相关的内容),而两个辅助点px和py在该坐标系上的坐标,正好就是它们在texture space上的uv坐标!

现在我们来看如何求辅助点px和py对应的uv坐标。首先假设该平面上有一点p′ ,如图10.4,我们可以通过计算下面的公式计算:


或者,等价于


我们看这个公式,除了𝚫u和𝚫v之外,其他参数都是已知的。把辅助坐标px代入p',求出的𝚫u和𝚫v就是偏导数(∂u/∂x, ∂v/∂x);把辅助坐标py代入p',求出的𝚫u和𝚫v就是偏导数(∂u/∂y, ∂v/∂y)。

书中后面讲了求解的过程及注意事项,因还有一些细节没弄明白,在此先不记录了,直接贴代码:

void SurfaceInteraction::ComputeDifferentials(
    const RayDifferential &ray) const {
    if (ray.hasDifferentials) {
        // Estimate screen space change in $\pt{}$ and $(u,v)$

        // Compute auxiliary intersection points with plane
        Float d = Dot(n, Vector3f(p.x, p.y, p.z));
        Float tx =
            -(Dot(n, Vector3f(ray.rxOrigin)) - d) / Dot(n, ray.rxDirection);
        if (std::isinf(tx) || std::isnan(tx)) goto fail;
        Point3f px = ray.rxOrigin + tx * ray.rxDirection;
        Float ty =
            -(Dot(n, Vector3f(ray.ryOrigin)) - d) / Dot(n, ray.ryDirection);
        if (std::isinf(ty) || std::isnan(ty)) goto fail;
        Point3f py = ray.ryOrigin + ty * ray.ryDirection;
        dpdx = px - p;
        dpdy = py - p;

        // Compute $(u,v)$ offsets at auxiliary points

        // Choose two dimensions to use for ray offset computation
        int dim[2];
        if (std::abs(n.x) > std::abs(n.y) && std::abs(n.x) > std::abs(n.z)) {
            dim[0] = 1;
            dim[1] = 2;
        } else if (std::abs(n.y) > std::abs(n.z)) {
            dim[0] = 0;
            dim[1] = 2;
        } else {
            dim[0] = 0;
            dim[1] = 1;
        }

        // Initialize _A_, _Bx_, and _By_ matrices for offset computation
        Float A[2][2] = {{dpdu[dim[0]], dpdv[dim[0]]},
                         {dpdu[dim[1]], dpdv[dim[1]]}};
        Float Bx[2] = {px[dim[0]] - p[dim[0]], px[dim[1]] - p[dim[1]]};
        Float By[2] = {py[dim[0]] - p[dim[0]], py[dim[1]] - p[dim[1]]};
        if (!SolveLinearSystem2x2(A, Bx, &dudx, &dvdx)) dudx = dvdx = 0;
        if (!SolveLinearSystem2x2(A, By, &dudy, &dvdy)) dudy = dvdy = 0;
    } else {
    fail:
        dudx = dvdx = 0;
        dudy = dvdy = 0;
        dpdx = dpdy = Vector3f(0, 0, 0);
    }
}

代码中的dudx,dvdx,dudy,dvdy就是我们要求的结果。

10.1.2 Filtering Texture Functions

我们必须要消除掉高于texture的Nyquist采样率的高频分量。如果使用ideal texture resampling(理想纹理重采样),为了让T (f (x, y))没有aliasing,我们首先必须对它进行band-limit(也就是消除掉超过的一定带宽范围的频率分量),通过用sinc filter进行卷积:


得到的Tb′(x,y)就是一个band-limit信号,然后用pixel filter g(x, y)对它做卷积,就可以得到我们想要的texture function:


但在实践中,我们通常会简化这些过程,一般只在band-limiting这一步使用一个box filter做处理,而第二步完全省略。这样的效果也不错。至于为啥,书中后面说了一大堆,说实话我已经没有耐心看了,先跳过吧...

10.1.3 选读内容,跳过

10.2 Texture Coordinate Generation(纹理坐标的生成)

再论坐标映射

这一小段是我自己加的,主要是读到这节后我发现纹理坐标映射相关的关系太多了,有必要理一理。看回SurfaceInteraction这个class,如下(删了不相关的代码):

class SurfaceInteraction : public Interaction {
  public:
    Point2f uv;
    Vector3f dpdu, dpdv;
    Normal3f dndu, dndv;
    mutable Vector3f dpdx, dpdy;
    mutable Float dudx = 0, dvdx = 0, dudy = 0, dvdy = 0;
};

你会看到有很多dxdx,加上本节的dstdx,dstdy,一共有十几个偏导数,让人很容易混淆。画了一个图来记录一下他们的映射关系。之前一直把uv坐标当做纹理坐标,其实这个并不准确,真正的纹理坐标应该是st坐标。而uv坐标是一个“虚拟”的2D坐标,范围恒定为(0, 0)~(1,1)。uv坐标和st坐标之间的映射 ,可以通过本节的TextureMapping2D来进行。


下面是正文内容

这一节纹理坐标的生成,说简单点就是,定义纹理“贴”到表面的方式(或者说映射的方式);或者反过来说,我们计算出了一个交点SurfaceInteraction,这个交点在纹理上对应的坐标是什么。可以看下图10.7的各种映射方式的区别:


从左到右是uv映射,球面映射,圆柱映射,平面映射。

TextureMapping2D class

代码如下:

class TextureMapping2D {
  public:
    // TextureMapping2D Interface
    virtual ~TextureMapping2D();
    virtual Point2f Map(const SurfaceInteraction &si, Vector2f *dstdx,
                        Vector2f *dstdy) const = 0;
};

TextureMapping2D只需要实现一个方法Map(),它输入一个SurfaceInteraction交点信息,返回值是该交点的纹理坐标(st坐标),并且通过参数指针dstdx和dstdy返回,在image space偏移一个像素对应在st坐标的变化率。

10.2.1 2D (u, v) Mapping

UVMapping2D定义uv坐标和st坐标的映射关系,它可以对uv坐标进行u方向和v方向分别进行缩放和偏移,来得到st坐标。代码较简单就不分析了,直接贴代码:

class UVMapping2D : public TextureMapping2D {
  public:
    // UVMapping2D Public Methods
    UVMapping2D(Float su = 1, Float sv = 1, Float du = 0, Float dv = 0);
    Point2f Map(const SurfaceInteraction &si, Vector2f *dstdx,
                Vector2f *dstdy) const;

  private:
    const Float su, sv, du, dv;
};

Point2f UVMapping2D::Map(const SurfaceInteraction &si, Vector2f *dstdx,
                         Vector2f *dstdy) const {
    // Compute texture differentials for 2D identity mapping
    *dstdx = Vector2f(su * si.dudx, sv * si.dvdx);
    *dstdy = Vector2f(su * si.dudy, sv * si.dvdy);
    return Point2f(su * si.uv[0] + du, sv * si.uv[1] + dv);
}
后面的SphericalMapping2D,CylindricalMapping2D,PlanarMapping2D以及3D Mapping暂不分析,需要再看。

10.3 Texture Interface and Basic Textures

Texture是一个模板类,模板参数是evaluation函数返回值的类型。pbrt目前只有Float和Spectrum两种类型。代码如下:

template <typename T>
class Texture {
  public:
    // Texture Interface
    virtual T Evaluate(const SurfaceInteraction &) const = 0;
    virtual ~Texture() {}
};

Texture class关键的接口是Evaluate(),它返回一个T类型的值,它唯一的输入信息来自即将被着色的点的SurfaceInteraction。

后面的ConstantTexture,ScaleTexture,MixTexture等都比较简单,直接看代码即可,在此略过。

10.4 Image Texture

开头书中说了一堆,这里不记录了,用我自己的话说,ImageTexture,是我们最常用的一种纹理贴图,它以RGB格式存储一张纹理图像的信息,将该纹理“贴”到要渲染的物体表面上,可以表现真实世界的各种物体的外貌。图10.8展示了image纹理的效果。


ImageTexture class

ImageTexture是一个模板类,它的模板参数既要定义存储在内存中的纹素的类型,也要定义它返回值的类型。代码如下:

template <typename Tmemory, typename Treturn>
       class ImageTexture : public Texture<Treturn> {
   public:
//ImageTexture Public Methods 
 private:
//ImageTexture Private Methods
}; 

例如ImageTextures存储RGBSpectrum的值在内存中,但通常返回Spectrum类型的值。用RGBSpectrum存储主要是可以节省大量的空间。

ImageTexture构造函数
template <typename Tmemory, typename Treturn>
ImageTexture<Tmemory, Treturn>::ImageTexture(
    std::unique_ptr<TextureMapping2D> mapping, const std::string &filename,
    bool doTrilinear, Float maxAniso, ImageWrap wrapMode, Float scale,
    bool gamma)
    : mapping(std::move(mapping)) {
    mipmap =
        GetTexture(filename, doTrilinear, maxAniso, wrapMode, scale, gamma);
}

//ImageTexture Private Data
std::unique_ptr<TextureMapping2D> mapping;
MIPMap<Tmemory> *mipmap;

caller需要提供一个图片的文件名filename,滤波的参数(doTrilinear和maxAniso),缩放(scale)以及是否gamma校正。文件的内容将会用来创建一个MIPMap实例,它是存储纹素并且处理重建和过滤的细节。

10.4.1 Texture Memory Management

每一个image map都可能需要相当数量的内存,而一个复杂的场景可能需要上千个image maps。因为在同一个场景中每一个image texture都可能被复用,pbrt持有一个table,记录到目前为止加载的image maps,这样就可以让他们只被加载一次,即使他们被多个ImageTexture。ImageTexture构造函数中调用静态函数ImageTexture:: GetTexture()来得到一个代表需要的texture的MIPMap。

template <typename Tmemory, typename Treturn> MIPMap<Tmemory> * ImageTexture<Tmemory, Treturn>::GetTexture(const std::string &filename,
           bool doTrilinear, Float maxAniso, ImageWrap wrap, Float scale,
bool gamma) 
{ 
//ReturnMIPMapfromtexturecacheifpresent
//Create MIPMap for filename 620⟩
return mipmap;
}

TexInfo是一个简单的结构体,记录了image map的所有参数,它主要是用于判断是有两个完全相同的image map,便于MIPMap被另一个ImageTexture复用。

//ImageTexture Private Data
static std::map<TexInfo, std::unique_ptr<MIPMap<Tmemory>>> textures;

//Return MIPMap from texture cache if present
TexInfo texInfo(filename, doTrilinear, maxAniso, wrap, scale, gamma);
if (textures.find(texInfo) != textures.end())
       return textures[texInfo].get();

如果texture还没有被加载,就调用ReadImage()来得到image contents。

//Create MIPMap for filename
Point2i resolution;
std::unique_ptr<RGBSpectrum[]> texels = ReadImage(filename, &resolution); MIPMap<Tmemory> *mipmap = nullptr;
if (texels) {
    //Convert texels to type Tmemory and create MIPMap
} else {
   //Create one-valued MIPMap
}
textures[texInfo].reset(mipmap);

因为ReadImage()返回一个RGBSpectrum类型的数组,如果MIPMap存储的类型不是RGBSpectrum(比如Float),就需要转换。这个转换由ImageTexture::convertIn()执行。
代码如下:

std::unique_ptr<Tmemory[]> convertedTexels(
        new Tmemory[resolution.x * resolution.y]);
for (int i = 0; i < resolution.x * resolution.y; ++i)
        convertIn(texels[i], &convertedTexels[i], scale, gamma);
mipmap = new MIPMap<Tmemory>(resolution, convertedTexels.get(),
                                     doTrilinear, maxAniso, wrap);

convertIn()代码中还有gamma校正的处理,在此不记录了。

10.4.2 ImageTexture Evaluation

ImageTexture::Evaluate()代码如下:

    Treturn Evaluate(const SurfaceInteraction &si) const {
        Vector2f dstdx, dstdy;
        Point2f st = mapping->Map(si, &dstdx, &dstdy);
        Tmemory mem = mipmap->Lookup(st, dstdx, dstdy);
        Treturn ret;
        convertOut(mem, &ret);
        return ret;
    }

首先用mapping->Map()计算纹理坐标st以及dstdx,dstdy,然后使用mipmap得到该st坐标对应的image纹理值,mipmap内部会处理过滤等抗混淆的事情。convertOut()会将最终返回的类型转换为Treturn类型。

10.4.3 Mip Maps

image function sampling rate和texture sampling rate

和往常一样,如果image function texture sampling rate比texture sampling rate的频率高,最终输出的图像会有aliasing。(看图10.9,image function也就是纹理图片,它的频率可以认为是纹理图片的分辨率,也就是图10.9的空心点;而texture sampling rate,是在最终输出的图像平面,对某点在x和y方向各偏移一个像素点,映射到texture纹理图像后的偏移,也就是图10.9的实心点)。在图10.9中,中间的黑点就是我们要lookup的(s, t)坐标,周围四个黑点与中间黑点的偏移距离可以认为是texture采样频率,我们的滤波器的过滤区域应该要覆盖这个偏移距离中的空心点,才能把image function的中的高频成分过滤到Nyquist频率之下。


texture采样和重建的过程,与第7章中的image采样过程有一些关键的不同点。这些不同让我们更高效的处理antialiasing问题有了可能。例如,得到一个采样点的值是很廉价的--熟悉要对一个数组查表(而与之相对的,在第7章image采样时我们需要追踪许多光线来计算radiance)。另外,因为texture image function全部是由采样点集合组成的,它的最高采样率不是秘密,因此在两个sample之间没有不确定的关系。这些不同点可以让我们在采样前进行过滤操作,因此也就消除了aliasing。

然而,pixel到pixel的texture sampling rate是会变化的。该sampling rate取决于场景中的几何形状,它的朝向,纹理坐标映射函数,和camera投影矩阵,以及image sampling rate。因为texture sampling rate不是固定的,texture filtering算法需要高效的过滤texture samples的任意区域。

两种滤波方法

MipMap class实现了两个滤波方法,可以过滤滤波半径会变化的区域。第一种是trilinear interpolation(三次插值),简单且速度快,在早期的硬件图形中广泛应用。第二种,elliptically weighted averaging(椭圆权重平均),较慢且较复杂,但可以得到高质量的结果。图10.1展示了这两种效果。


图像金字塔

这两种滤波方法都使用了一种叫image pyramid(图像金字塔)的技术。如下图(图片来自《Real-Time Rendering Third Edit》):


原始image texels是在金字塔的最底层,每一层的image的分辨率是前一层的一般,到最顶层,只有一个单一的代表原始image所有像素平均值的纹素。这个images的集合需要将近三分之一额外的内存,但可以被用来快速查找原始image中需要过滤的一大块区域而得到的值。这个金字塔背后的思想是,如果需要对原始image过滤一大块区域的纹素,可以通过使用金字塔中一个更高层级的image来近似,这样可以减少访问的纹素数量。

MIPMap class

tip:后面的没整理,纯粹进行劣质翻译...
在MIPMap构造函数中,MIPMap拷贝caller提供的image data,如果image的分辨率不是2的N次方,则会对image进行resize。初始化一个lookup table,用于第10.4.5节的elliptically weighted average filtering method。并且记录了当texture坐标超出规定范围时,根据wrapMode的行为。

template <typename T>
MIPMap<T>::MIPMap(const Point2i &res, const T *img, bool doTrilinear,
                     Float maxAnisotropy, ImageWrap wrapMode)
       : doTrilinear(doTrilinear), maxAnisotropy(maxAnisotropy),
         wrapMode(wrapMode), resolution(res) {
       std::unique_ptr<T[]> resampledImage = nullptr;
       if (!IsPowerOf2(resolution[0]) || !IsPowerOf2(resolution[1])) {
//Resample image to power-of-two resolution
 }
//Initialize levels of MIPMap from image
//Initialize EWA filter weights if needed 639⟩ 
}

//MIPMap Private Data
const bool doTrilinear;
const Float maxAnisotropy;
const ImageWrap wrapMode;
Point2i resolution;

MIPMap是一个模板类,模板参数是image texels的类型。pbrt可以创建RGBSpectrum和Float类型的MIPMaps;例如,Float MIP maps用来代表光源发出的光照强度的方向分布(12.3.3节)。

ImageWrap参数,用来定义当提供的纹理坐标超出[0,1]范围后的行为。

//MIPMap Helper Declarations
enum class ImageWrap { Repeat, Black, Clamp };
image resize

如果原始image的分辨率是2的N次方,实现一个image金字塔相对来说更容易一些。如果提供的image不是2的N次方,MIPMap构造函数会将image重新缩放到下一个大于当前image分辨率的2的N次方的尺寸。

这里的Image resizing涉及到比第7章的采样和重建理论更多的应用:我们有一个image function,它是以单像素的采样频率采样过的。现在我们想把它重建为一个以新的采样位置采样的连续image。因为新的采样率会比原来的更高(因为分辨率更高),我们不需要担心欠采样而引发的aliasing问题;我们只需要直接重采样和重建一个新的function即可。如图10.11.


后面的感觉还是太多了,暂时就不记录了,后面真用上的话再看吧...

相关文章

网友评论

      本文标题:pbrt笔记--第十章 纹理

      本文链接:https://www.haomeiwen.com/subject/mpeywctx.html