热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

用C语言画光

之前写过用C语言画心形、圣诞树、雪花、直线,这次我们试玩光学。在这系列文章中,我们会在二维的画布上「绘画」一些基于物理的光。但作为趣味性的编程文章,我尽量用直觉、简单的方式去生成这些图像

之前写过用 C 语言画心形、圣诞树、雪花、直线,这次我们试玩光学。

在这系列文章中,我们会在二维的画布上「绘画」一些基于物理的光。但作为趣味性的编程文章,我尽量用直觉、简单的方式去生成这些图像,仅介绍一些概念,和一般的正式计算机图形学内容不同。

本系列的源代码位于 miloyip/light2d

1. 光

假设我们在一个二维的世界,这里有些会发光的二维形状,并暂时只考虑单色光。我们想知道的是,在这个空间中,每一点从 360 度各方向共有多少光经过。换成数学方式表示,我们想对每个二维坐标 , "wb"), W, H, img, 0);
}

无论图像长宽是多少,这个二维空间的坐标范围都是 (x, y) \in [0, 1] \times [0, 1] 。我们把结果映射至 \{0, 1, \ldots, 255\} 。

2. 蒙地卡罗积分

由于无法以解析式求解这个积分,我们使用蒙地卡罗积分法(Monte Carlo integration)。

在这个问题中,我们随机采样 N 个方向 {\theta_1, \theta_2, \ldots, \theta_N} ,然后计算 L(x, y, \theta_i) 的平均值:

F(x, y) \approx \frac{2\pi}{N}\sum_{i=1}^N L(x, y, \theta_i)

代码实现也很浅白(设每像素向 64 个随机方向均匀采样/uniform sampling):

#define TWO_PI 6.28318530718f
#define N 64

float sample(float x, float y) {
float sum = 0.0f;
for (int i = 0; i < N; i++) {
float a = TWO_PI * rand() / RAND_MAX;
sum += trace(x, y, cosf(a), sinf(a));
}
return sum / N;
}

当中 \texttt{trace(ox, oy, dx, dy)} 函数代表从 \mathbf{o} 位置从单位矢量 \hat{\mathbf{d}} 方向接收到的光。

(更新:本文不考虑实际单位,所以实现时把系数 2\pi 去掉了。感谢 @Bimos 提醒)

3. 光线步进

通常,我们可以用光线追踪(ray tracing)方法,求出光线 \mathbf{r}(t) = \mathbf{o} + \hat{\mathbf{d}}t 与场景的最近点。

然而,我们需要为每种几何形状编写与光线的相交函数(通常比较复杂),之后做一些效果可能还要提供相交点的法线(normal vector)。

为简单起见,本文采用光线步进(ray marching)方法(又称为球体追踪/sphere tracing [1]),场景只需要以带符号距离场(signed distance field, SDF) \phi : \mathbb{R}^2 \rightarrow \mathbb{R} 表示

  1. 当 \phi(\mathbf{x}) > 0 ,表示坐标 \mathbf{x} 位于场景形状之外,且 \mathbf{x} 与最近形状边界的距离为 \phi(\mathbf{x}) ;
  2. 当 \phi(\mathbf{x}) <0 ,表示坐标 \mathbf{x} 位于场景形状之内,且 \mathbf{x} 与最近形状边界的距离为 -\phi(\mathbf{x}) ;
  3. 当 \phi(\mathbf{x})= 0 ,说明 \mathbf{x} 刚好在形状边界上。

例如,圆心为 \mathbf{c} 、半径为 r 的圆形 SDF 定义为(更精确的说法是圆盘/disk):

\phi_\text{circle}(\mathbf{x}) = \left \| \mathbf{x} - \mathbf{c} \right \|-r

用代码表示:

float circleSDF(float x, float y, float cx, float cy, float r) {
float ux = x - cx, uy = y - cy;
return sqrtf(ux * ux + uy * uy) - r;
}

而所谓光线步进,就是我们从光线起始点 t = 0 ,逐步增加 t ,当 \phi(\mathbf{r}(t)) \le 0 代表我们到达到场景中某个形状的表面或内部。那么我们每次可以步进多远?由于 \phi(\mathbf{x})>0 时,代表 \mathbf{x} 距最近形状的距离,所以我们至少可以步进该距离而不会碰到任何形状!

图源 https://developer.nvidia.com/gpugems/GPUGems2/gpugems2_chapter08.html

设场景只有一个发光的圆形,圆心为 (0.5, 0.5) ,半径为 0.1 ,它每点都向各方向发射 2个单位的光。那么光线步进可实现为:

#define MAX_STEP 10
#define MAX_DISTANCE 2.0f
#define EPSILON 1e-6f

float trace(float ox, float oy, float dx, float dy) {
float t = 0.0f;
for (int i = 0; i < MAX_STEP && t < MAX_DISTANCE; i++) {
float sd = circleSDF(ox + dx * t, oy + dy * t, 0.5f, 0.5f, 0.1f);
if (sd < EPSILON)
return 2.0f;
t += sd;
}
return 0.0f;
}

如果光线超过指定距离,或是步数太多,都终止步进。注意我们只能尽量接近表面,所以用 \epsilon = 10^{-6} 表示足够近的阈值。整个程序完成,运算结果如下:

可以看到图像中有很多噪点,这是由于蒙地卡罗积分法具随机性,计算出来的估值与精确数值会有误差。增加采样数目 N ,就能令结果更精确(准确地说是减少方差/variance):

然而,随着 N 上升,运算时间也线性上升。有没有方法可以改善?

4. 分层、抖动采样

形成噪点的原因在于,每个像素所得到的随机方向都很不一样。那么,如果我们不用随机方向,而是平分 360 度向 N 个方向采样,效果会如何?这种方式称为分层采样(stratified sampling):

float sample(float x, float y) {
float sum = 0.0f;
for (int i = 0; i < N; i++) {
// float a = TWO_PI * rand() / RAND_MAX; // 均匀采样
float a = TWO_PI * i / N; // 分层采样
// ...

改变这一行代码的结果是:

很好,没有噪点,但也太过规律了!

我们可以结合上面两种采样方式,就是先分为 N 个角度区域,再从区域中均匀采样,这称为抖动采样(jittered sampling)。

// float a = TWO_PI * rand() / RAND_MAX; // 均匀采样
// float a = TWO_PI * i / N; // 分层采样
float a = TWO_PI * (i + (float)rand() / RAND_MAX) / N; // 抖动采样

改变这一行代码的结果是:

同样使用每像素 N=64 次采样,仅仅改变采样方式(一行代码),效果就好很多:

5. 结语

这个完整程序位于 basic.c。如果不含加载头文件及常数定义,仅有 30 行代码。你可以改一下圆形位置、大小、光度,测试时可用较小的 W、H 和 N 加速运行过程。

本文简单介绍了蒙地卡罗积分、光线步进、分层采样等概念。下一篇《构造实体几何》会讲如何在场景中加入更多形状,将会显示出阴影。而之后也会尝试实现一些光学法则,生成更有趣的图形。

参考

[1] Hart, John C. "Sphere tracing: A geometric method for the antialiased ray tracing of implicit surfaces." The Visual Computer12.10 (1996): 527-545.


https://zhuanlan.zhihu.com/p/30745861


推荐阅读
  • 本文介绍了Python语言程序设计中文件和数据格式化的操作,包括使用np.savetext保存文本文件,对文本文件和二进制文件进行统一的操作步骤,以及使用Numpy模块进行数据可视化编程的指南。同时还提供了一些关于Python的测试题。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • Carve库在Visual Studio2015中的编译方法及注意事项
    本文介绍了在Visual Studio2015中编译Carve库的方法及注意事项。首先下载Carve库,并使用Visual Studio2015打开,生成后在bin目录下会生成.lib文件。同时,本文还指出了之前在Visual Studio2017中编译的问题,并提醒需要根据对应的平台进行编译,否则会出现报错。详细的步骤和注意事项请参考原文链接。 ... [详细]
  • Python脚本编写创建输出数据库并添加模型和场数据的方法
    本文介绍了使用Python脚本编写创建输出数据库并添加模型数据和场数据的方法。首先导入相应模块,然后创建输出数据库并添加材料属性、截面、部件实例、分析步和帧、节点和单元等对象。接着向输出数据库中添加场数据和历程数据,本例中只添加了节点位移。最后保存数据库文件并关闭文件。文章还提供了部分代码和Abaqus操作步骤。另外,作者还建立了关于Abaqus的学习交流群,欢迎加入并提问。 ... [详细]
  • 初探PLC 的ST 语言转换成C++ 的方法
    自动控制软件绕不开ST(StructureText)语言。它是IEC61131-3标准中唯一的一个高级语言。目前,大多数PLC产品支持ST ... [详细]
  • 初始化初始化本地空版本库,仓库,英文名repositorymkdirtest&&cdtestgitinit克隆项目到本地gitclone远程同 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 如何实现织梦DedeCms全站伪静态
    本文介绍了如何通过修改织梦DedeCms源代码来实现全站伪静态,以提高管理和SEO效果。全站伪静态可以避免重复URL的问题,同时通过使用mod_rewrite伪静态模块和.htaccess正则表达式,可以更好地适应搜索引擎的需求。文章还提到了一些相关的技术和工具,如Ubuntu、qt编程、tomcat端口、爬虫、php request根目录等。 ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 如何去除Win7快捷方式的箭头
    本文介绍了如何去除Win7快捷方式的箭头的方法,通过生成一个透明的ico图标并将其命名为Empty.ico,将图标复制到windows目录下,并导入注册表,即可去除箭头。这样做可以改善默认快捷方式的外观,提升桌面整洁度。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • HTML5网页模板怎么加百度统计?
    本文介绍了如何在HTML5网页模板中加入百度统计,并对模板文件、css样式表、js插件库等内容进行了说明。同时还解答了关于HTML5网页模板的使用方法、表单提交、域名和空间的问题,并介绍了如何使用Visual Studio 2010创建HTML5模板。此外,还提到了使用Jquery编写美好的HTML5前端框架模板的方法,以及制作企业HTML5网站模板和支持HTML5的CMS。 ... [详细]
  • 1.脚本功能1)自动替换jar包中的配置文件。2)自动备份老版本的Jar包3)自动判断是初次启动还是更新服务2.脚本准备进入ho ... [详细]
author-avatar
北极光的悲伤
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有