Pixel World

it's better be burning out than to fade away.

计算机图形学(十一):信号处理

在图形学中,通常要处理些连续变量的函数:图片是你见过的第一个例子,但是当你继续探索图形学的时候,你会遇到很多类似的情况.就其本质而言,计算机无法直接表达连续的函数,必须使用有限的位数表达他们- 采样(将函数的值离散的存储起来,在需要时进行重建).
本章首先用数字音频的具体一维例子来总结采样和重构。然后,我们继续介绍在一维和二维的采样和重构基础上的基本数学和算法。最后,我们深入频域观点的细节,它为这些算法的行为提供了许多见解。

数字音频: 一维采样

尽管采样在电信领域已经应用了很多年,但随着数字音频的使用增加,1982年cd的推出是采样在消费者中的第一次大规模应用.
alt
在录音过程中,麦克风将空气中以压力波形式存在的声音转换成时变的电压,这相当于在麦克风所在的位置测量空气压力的变化。这种电信号需要以某种方式储存起来,以便在以后的某个时间播放,并发送到扬声器中,通过与电压同步移动膜片,将电压转换回压力波.
录制音频的过程中使用了采样技术, 模数转换器 analog-to-digital converter (A/D converter, or ADC)每秒钟数千次测量电压,生成容易被存储下来的数据流(例如记录在计算机硬盘中).
在回放时,数据流以适当的速率被读取,并被发送到数模转换器 digital-to-analog converter (D/A converter, or DAC),DAC根据它接收到的数字产生一个电压,并且如果我们取足够多的样本来表示电压的变化,所得到的电信号,都是相同的输入信号.
事实证明,每秒钟需要多少样本来完成一次良好的录制,取决于我们试图记录的声音有多高。一个可以很好录制弦乐低音或者鼓的采样率,应用于录制短笛或铙钹,就会产生奇怪的结果;但这些声音在较高的采样率下被录制得很好。为了避免这些采样不足的影响,数字音频记录器对ADC的输入进行滤波,去除可能导致问题的高频。
另一种问题出现在输出端。DAC产生的电压在新采样进入时发生变化,但在下一个采样进入前保持不变,产生阶梯型的波形。这些楼梯就像噪音一样,增加了一种高频的、依赖于信号的嗡嗡声。为了消除这种重建带来的问题,数字音频播放器过滤DAC的输出以平滑波形.

采样偏差与失真

数字音频记录链可以作为采样和重建过程的具体模型,发生在图形中。同样的欠采样和重构伪影也会发生在图像或图形中的其他采样信号上,解决方法是一样的:采样前进行滤波,重构时再进行滤波.
alt
上图显示了一个由过低的采样频率导致的走样具体例子。在这里,我们用两种不同的采样频率对一个简单的正弦波进行采样:顶部的高频采样和底部的低频采样。高频采样显然能够更好的还原信号,但是由低采样率产生的样本与低频正弦波的样本是无法区分的.
一旦采样完成,就无法区分两个信号——快正弦波和慢正弦波——哪个是原始信号,因此没有单一的方法可以在这两种情况下正确地重建信号。因为高频信号可以“假装”成低频信号,这种现象被称为失真。
在图形领域,失真经常表现为:

  • 摩尔纹(moire patterns)
  • 边缘锯齿

采样和重构的基本问题可以简单地根据特征太小或太大来理解,但一些更定量的问题很难回答:

  • 多高的采样率能够还原重建结果?
  • 什么样的滤波器适合于采样和重建?
  • 为了避免失真,需要多大程度的平滑过渡?
阅读全文 »

计算机图形学(十):柏林噪声

$Perlin\;Noise$

1985年,$Ken\;Perlin$发表了一篇名为《A Image Synthetizer》的Siggraph学术论文。提出了一种类似于之前介绍的噪声函数算法,但是表现更好。柏林噪声和之前的噪声函数很相似,和$Value\;Noise$一样,它也依赖于网格系统。在网格的每个顶点处定义随机值,然后对其进行插值以计算空间中某个位置的噪声值。之前我们详细介绍了1D和2D$Value\;Noise$的创建。本章将实现$Perlin\;Nosie$的3D版本。那么$Perlin\;Nosie$和$Value\;Noise$有什么区别呢?对于$Value\;Noise$来说,我们只需在网格的顶点处分配随机数,采样点对所在单元格四个顶点做双线性插值即可。在$Perlin\;Nosie$中,$Ken\;Perlin$建议将网格顶点的随机值替换为梯度(归一化的三维向量),方法是在$[0,1]$范围内生成三个随机浮点数,再将这些随机数重映射到$[-1,1]$范围内,最后对向量进行归一化。

1
2
3
4
5
6
const tableSize = 10;
const gradients = [];
for (let i = 0; i < tableSize; i++) {
gradients[i] = vec3(2 * Math.random() - 1, 2 * Math.random() - 1, 2 * Math.random() - 1);
gradients[i] = gradients[i].normalize();
}

实现

相比通过随机数插值的$Value\;Noise$,$Perlin\;Nosie$在网格顶点分布的是三维向量,由于噪声函数需要返回一个浮点数,如何通过三维向量生成浮点数呢?$Ken\;Perlin$建议计算每个网格顶点到我们要计算的点$p$的方向向量,我们可以得到8个3D向量(一个立方体有8个角或顶点)和4个2D向量(二维)。然后计算网格顶点的梯度和从该顶点到$p$点向量的点积。由于向量点积返回一个浮点数,因此,我们将梯度或向量转换为了浮点数。与2D情况一样,为了计算点$p$在3D网格中的坐标,我们将点坐标从浮点数转换为整数值($floor$),然后将这些整数坐标取模$N$,即随机方向的数组(N = 256)。正如在上一章中解释的那样,我们不想生成256x256x256方向的网格。使用包含256个随机方向的单个一维数组,并使用置换表通过将点的整数坐标转换为带有哈希的置换表的索引来“随机拾取”存储在该表中的方向之一。

代码实现如下:

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
const tableSize = 256;
const tableSizeMask = tableSize - 1;
const gradients = [];
const permutationTable = [];

class PerlinNoise {
constructor() {
for (let i = 0; i < tableSize; i++) {
gradients[i] = vec3(2 * Math.random() - 1, 2 * Math.random() - 1, 2 * Math.random() - 1);
gradients[i] = gradients[i].normalize();
permutationTable[i] = i;
}

for (unsigned i = 0; i < tableSize; ++i)
std::swap(permutationTable[i], permutationTable[diceInt() & tableSizeMask]);
// extend the permutation table in the index range [256:512]
for (unsigned i = 0; i < tableSize; ++i) {
permutationTable[tableSize + i] = permutationTable[i];
}
}

hash(x: number, y: number, z: number) {
return this.permutationTable[this.permutationTable[this.permutationTable[x] +y ] + z]
}

computeX(p: vec3) {
const xi0 = Math.floor(p.x) & tableSizeMask;
const yi0 = Math.floor(p.y) & tableSizeMask;
const zi0 = math.floor(p.z) & tableSizeMask;

const xi1 = (xi0 + 1) & tableSizeMask;
const xi1 = (xi0 + 1) & tableSizeMask;
const xi1 = (xi0 + 1) & tableSizeMask;

const tx = p.x - Math.floor(p.x);
const ty = p.y - Math.floor(p.y);
const tz = p.z - Math.floor(p.z);

const u = smoothStep(tx);
const v = smoothStep(tx);
const w = smoothStep(tx);

const c000 = gradients[hash(xi0, yi0, zi0)];
const c100 = gradients[hash(xi1, yi0, zi0)];
const c010 = gradients[hash(xi0, yi1, zi0)];
const c110 = gradients[hash(xi1, yi1, zi0)];

const c001 = gradients[hash(xi0, yi0, zi1)];
const c101 = gradients[hash(xi1, yi0, zi1)];
const c011 = gradients[hash(xi0, yi1, zi1)];
const c111 = gradients[hash(xi1, yi1, zi1)];

const x0 = tx, x1 = tx - 1;
const y0 = ty, y1 = ty - 1;
const z0 = tz, z1 = tz - 1;

const p000 = vec3(x0, y0, z0);
const p100 = vec3(x1, y0, z0);
const p010 = vec3(x0, y1, z0);
const p110 = vec3(x1, y1, z0);

const p001 = vec3(x0, y0, z1);
const p101 = vec3(x1, y0, z1);
const p011 = vec3(x0, y1, z1);
const p111 = vec3(x1, y1, z1);

// linear interpolation
const a = lerp(dot(c000, p000), dot(c100, p100), u);
const b = lerp(dot(c010, p010), dot(c110, p110), u);
const c = lerp(dot(c001, p001), dot(c101, p101), u);
const d = lerp(dot(c011, p011), dot(c111, p111), u);

const e = lerp(a, b, v);
const f = lerp(c, d, v);

return lerp(e, f, w);
}
}

为了平滑插值,我们在使用tx ty tz之前对其分别使用smoothStep函数重新映射为uvw。
通过代码可以看出,$Perlin\;Nosie$和$Value\;Noise$非常类似,唯一不同的是我们将网格顶点的随机值替换为随机的单位向量,然后计算顶点梯度和顶点到当前点的方向向量的点积。

阅读全文 »

计算机图形学(九):噪声-下

$2D\;Noise$

在上一章中,我们介绍并解释了实现一维噪声函数的大部分技术和方法。创建更高维度的噪声和一维噪声并没有本质上的区别,因为它们都是基于同样的方法和技术。之前提到过,所有的噪声函数都会返回一个浮点数,无论输入参数是浮点数,二维点还是三维点,至于一维噪声 二维或三维噪声仅与其输入值有关。二维噪声是将二维点作为输入参数的噪声函数。
对于二维噪声,我们会在网格顶点处分布随机值。噪声函数的2D版本以2D点作为输入,假设当前要求的点为$p$。与一维版本类似,我们先得找到点$p$在网格中的位置,如果点在网格之外,可以通过相同的取模技巧重新映射$p$点的位置,得到网格上点$p$的新坐标,记作$Pnoise$。
之前的章节中讲过插值相关的很多内容。

2dNoise

如下图所示:点$Pnoise$被一个单元上的四个顶点所包围。使用之前学到过的双线性插值技术可以轻松得到点$Pnoise$的值(周围四个点的加权平均):

2dNoise

为此我们首先计算$s和t$,它们是一维噪声中t的对应物:

2dNoise

代码实现如下:

阅读全文 »

计算机图形学(八):噪声-上

本章将介绍噪声的基础内容,包括噪声是什么,它的属性以及可以用来做什么。噪声不是一个难以理解的复杂概念,但它有许多微妙之处需要注意。 正确使用它需要了解它的工作原理和实现方式。 为了创建一些图像并使用各种参数进行实验,我们将实现一个简单(但功能齐全)的噪声函数,称为$Value Noise$。 本章我们将忽略许多过于复杂而无法在此处全面研究的技术,只是对噪声及其一些应用的初步介绍。

历史背景

噪声是在80年代中期发明的,起初是作为图像纹理的替代方法。主要原因在于80年代中期的电脑内存有限,无法容纳用于纹理映射的图片,于是人们开始寻找替代解决方案。用纯色渲染物体看起来太无趣了,需要通过调整物体的表面材质属性来打破这种干净的外观。在编程中,我们通常在需要创建随机时使用伪随机数生成器。然而使用$RNG(Random\;number\;generator)$是远远不够的。我们在自然界中观察到的随机模式通常是很自然的,物体表面上距离很近的两个点通常看起来会比较相似。但是同一物体上相距很远的两个点却差异很大。换句话说:局部变化是细微的,是渐进的,而全局变化则是很大的。$RNG$无法满足这个要求,因为每次调用随机数生成器它会返回和其它数值完全不相干的随机数。因此调用这个函数会产生两个完全不同的数字,最终产生杂乱无章的白噪声。下面是一个例子:让我们观察一块真实岩石的图像,假设我们想要复现该CG图像。这个例子很有趣,因为我们可以看到岩石图案由三种颜色组成:绿色,粉色和灰色。这些颜色或多或少分布在岩石表面上。我们首先使用随机数生成器的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void GenerateRandPattern() 
{
unsigned imageWidth, imageHeight;
imageWidth = imageHeight = 512;
static const unsigned kNumColors = 3;
Color3f rockColors[ kNumColors ] = {
{ 0.4078, 0.4078, 0.3764 },
{ 0.7606, 0.6274, 0.6313 },
{ 0.8980, 0.9372, 0.9725 } };
std::ofstream ofs( "./rockpattern.ppm" );
ofs << "P6\n" << imageWidth << " " << imageHeight << "\n255\n";
for ( int j = 0; j < imageWidth; ++j )
{
for ( int i = 0; i < imageHeight; ++i )
{
unsigned colorIndex = std::min( unsigned( drand48 () * kNumColors ), kNumColors - 1 );
ofs << uchar( rockColors[ colorIndex ][ 0 ] * 255 ) <<
uchar( rockColors[ colorIndex ][ 1 ] * 255 ) <<
uchar( rockColors[ colorIndex ][ 2 ] * 255 );
}
}
ofs.close();
}

RNG

上图中间的图像是程序生成的,结果并不理想,实际上这个模式有个名字,白噪声($white\;Noise$)(稍后会解释什么是白噪声)。使用$RNG$为程序生成纹理的每个像素随机选择一种颜色,使得结果中每个像素的颜色变化很大。为了改进结果,我们复制了纹理的一小块区域(10x10像素),将其调整到原始图像的尺寸(256x256),进行高斯模糊处理,生成的图像如上右所示,局部有了较小的变化,而全局变化比较明显。

这个实验结论告诉我们,为了创建平滑的随机图案,需要在网格的固定位置上使用$RNG$分配随机数,然后使用高斯模糊来模糊这些值。下章将详细介绍如何对随机数做模糊,现在只需要记住:噪声(在图形学上下文中)是一个用于模糊在网格上生成的随机数的函数。

目前应用最广泛的噪声版本是由$Ken\;Perlin$在1983年实现的,称为柏林噪声($Perlin\;Noise$)当时他正在制作1982年版本的电影 Tron(© Walt Disney Pictures)。肯·柏林 (Ken Perlin) 于1997年获得美国电影艺术与科学学院颁发的奥斯卡技术成就奖,以表彰他对这部电影的贡献。 他于1984年在Siggraph展示了他的工作,并于1985年发表了一篇论文《An Image Synthesiser》,这是程序化纹理开创性的成果。Perlin噪声会在后续章节中进行解释。

阅读全文 »

计算机图形学(七):阴影

All the variety, all the charm, all the beauty of life is made up of light and shadow - Tolstoy

阴影对于表达场景真实感极其重要,因为它能够提供物体在空间中的相对位置关系,使物体看起来不是漂浮在空中的。本章将重点介绍计算阴影理论以及在光栅化中实时阴影算法。

shadows

《古墓丽影-暗影》 2018

之前介绍$blinn-Phong\;Model$光照模型是局部的,仅考虑光线 着色点 视线三个因素,不考虑其它物体对于当前着色点的影响,例如遮蔽,阴影等,而现实情况是,光照是及其复杂的,需要考虑周围物体对着色点的影响(间接光照),而在传统的局部着色(直接光照)中很难实现准确的表达,往往需要通过其它技术近似的模拟,今天介绍的$shadow\;Mapping$就是其中之一。一种在光栅化成像中实现阴影的技术。

$Shadow\;Mapping$

它是一种图像空间($Image-Space$)算法。核心思想就是:那么一个着色点既可以被摄像机看到也可以被光源看到,那么该点不在阴影里。如果一个着色点在阴影里,那么摄像机可以看到,光源是看不到的。

传统的$Shadow\;Mapping$只能处理点光源,这样的阴影都有明显的边界和锯齿,一个着色点要么在阴影里,要么不在,缺少了中间柔和的过渡。这种阴影我们称之为硬阴影

阅读全文 »

计算机图形学(六):反射方程与渲染方程

双向反射分布函数($BRDF$)

描述

$BRDF$全称$Bidirectional\;Reflectance\;Distribution\;Function$。$BRDF$是描述了物体表面对能量反射分布特性的函数。假设物体表面单位面积$dA$接收到来自$W_i$方向光线的能量,用$E$来表示,这些能量$E$到达物体表面后,会向各个不同的方向反射(每个方向反射的能量不同),$BRDF$函数就是描述这束能量向特定方向$W_r$的反射占比。

BRDF

  • Differential irradiance incoming:$dE(w_i) = L(w_i)\cos\theta_i{d}w_i$
  • Differential intensity exiting: $dL_r(w_r)$

正式定义

单位面积$dA$从单位立体角$w_i$接收到的$irradiance$,会被如何反射到各个不同的方向上去。更通俗点来说就是BRDF描述的是单位面积接收单位立体角$w_i$的能量的反射分布(比例)。反射比例依赖于出射方向($w_r$)。$BRDF$描述了光与物体表面是如何相互作用的。即物体的材质属性由$BRDF$定义。

BRDF

阅读全文 »

计算机图形学(五):纹理应用-下

上一章介绍了凹凸贴图,凹凸贴图可以提升粗糙mesh细节表现,凹凸贴图其实没有改变表面的几何结构,仅仅是通过法线的扰动影响了着色而已,因此产生的问题就是,在特定角度观察或者观察物体边缘或阴影时,问题就暴露出来了,这是因为没有考虑高程带来的视差和遮挡。因为mesh信息没有提供对应的深度变化,在$z-buffer$深度测试时,不会对被遮挡的顶点做深度测试,因此不会产生应有的自遮挡现象。

bummmap

视差贴图 ($Parallax\;Mapping$)

视差贴图解决了凹凸贴图带来的问题(凹凸贴图没有考虑自遮挡问题),如下图当我们从相机观察到$P$点时,由于高程的存在,我们应该看到的是$p_ideal$,但实际上在凹凸贴图用的是p点的着色信息,所以看起来不够真实:

bummmap

2001年,$Kaneko$引入了视差贴图的概念,并对其进行了改进并由$Welsh$普及。 视差是指当观察者移动时,对象彼此相对移动。 随着观察者的移动,凹凸应该看起来有高度。 视差映射的关键思想是采用通过实时检查像素的高度来有根据地猜测应该在像素中看到什么(遮蔽现象)。

$Parallax\;Mapping$所需要的信息通常存储在一张高度图($heightfield\;Texture$)中。当观察物体表面某一着色点时,首先会根据纹理查询该点对应的高度值,然后基于这个高度值和观察角度计算出一个偏移量$offset$,这个偏移量实际上就是$p$点的偏移量,即我们实际上应该看到的像素点$p_{adj}$,而不是$p$点。如上右图所示,所做的事情就是:根据$p$点的高程,以及 $v$的方向,对$p$点偏移一段距离到达$p_{adj}$,以接近$p_{ideal}$

$p_{adj} = p + \dfrac{h*v_{tb}}{v_n}$

阅读全文 »

计算机图形学(四):纹理应用-上

在计算机图形学中,纹理贴图是使用图像、函数或其他数据源来改变物体表面外观的技术。例如,可以将一张砖墙贴图应用到一个多边形上,而不用对砖墙的几何形状进行精确建模。通过这种方式将图像和物体表面结合起来,可以在建模、存储空间和速度方面节省很多资源。当然纹理不仅仅用于改变物体表面漫反射颜色,纹理本质上是一个数据集,可以用来存储任何我们需要的数据,因此在各方面都得到了广泛的应用.本文将介绍MaterialMap、AlphaMap、BumpMap、NormalMap、ReliefMap、DisplacementMap、ParallaxMap、TexturedLight、ShadowMap、EnvironmentMap。
我们将以上纹理贴图的应用划分为6个大类:

  • 控制着色信息
  • 控制片元透明度
  • 改变顶点法线
  • 改变表面结构
  • 阴影贴图
  • 环境贴图

控制着色信息

根据$Blinn-phong\;Model$可知,物体表面着色信息有以下因子来控制:

blinn-phong

$L = L_a+L_d+L_s = K_aI_a + K_d\dfrac{I}{r^2}max(0, \hat{n}\cdot\hat{l}) + K_s\dfrac{I}{r^2}max(0, \hat{n}\cdot\hat{h})^p$

符号 解释
$K_a$ 物体表面环境光吸收率
$I_a$ 环境光强度
$\dfrac{I}{r^2}$ 到达物体表面的光线
$K_d$ 漫反射系数
$\hat{n}$ 着色点法线
$\hat{l}$ 光线单位向量
$K_s$ 高光系数
$p$ 高光衰减因子

可以看到有许多可以调节的参数来控制像素的着色表现。虽然可以赋予顶点更多的属性来改变这些参数,但是要做到亚三角形的细节程度,就需要使用各种纹理,对每个片元的着色参数进行调节,这些纹理映射方法统称为材质映射($Material\;Map$)。

阅读全文 »

计算机图形学(三):纹理映射

纹理管线($The\;Texture\;Pipeline$)

纹理映射 ($Texture Mapping$) 是一种将物体空间坐标点转化为纹理坐标,进而从纹理上获取对应纹素值,以增强着色细节的方法。

纹理管线($The\;Texture\;Pipeline$), 也就是单个纹理应用到纹理贴图的详细过程,其中每一个步骤均可以被用户灵活控制。

纹理映射步骤

一个砖墙的纹理管线处理过程。

纹理映射步骤

假设拥有三维模型空间到二维纹理空间的映射关系,只需要将每个顶点的颜色信息存储在二维纹理贴图上,在进行光照计算时根据映射关系查询每个顶点的颜色信息。所有点计算完后,就好像整个纹理被贴到了物体表面一样。但是如何建立这种映射关系呢?接下来介绍两种方式。

投影映射-从物体坐标系到参数空间($Projector\;and\;Mapping$)

阅读全文 »

计算机图形学(二):着色

渲染过程本质可以上分解为两个步骤:可见性测试和着色。光栅化和光线追踪本质上都是用于解决可见性问题。本章我们将介绍渲染过程的第二部分:着色。着色是一个庞大的主题。一些用于着色的技术在数学上也很复杂。本章我们将学习最基本的着色模型: $Blinn-Phong\;Reflectance\;Model$

$blinn-Phong\;Reflectance\;Model$

当我们观察一个物体时,可以明显的将物体的视觉表现分为三类:

  • 高光:表现很亮的部分,并且会随着视角的变化而变化
  • 漫反射部分:颜色变化不明显,表示物体本身的颜色或者diffuse颜色。(物体表面吸收了部分光线,反射出的未被吸收的部分)
  • 环境光:未被光照直接照亮的部分,模拟间接光照。在布林冯模型中简化为常量。这也是为什么布林冯模型被称为经验模型的原因。计算精确的间接光照需要$RayTracing$。

blinn-phong

布林冯光照模型是一个经验模型。整体思想就是按照这三部分来建模的。布林冯光照模型是一个直接光照模型(或者叫局部光照/局部性),不考虑间接光照,即不考虑周围物体反射的光线对其产生的影响。着色权重仅考虑光源。(阴影及SSAO需要多个pass处理)

定义光照参数

要计算物体表面着色点光照结果,需要定义一些基本的参数。

阅读全文 »
0%