热门标签 | 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


推荐阅读
  • 通过Anaconda安装tensorflow,并安装运行spyder编译器的完整教程
    本文提供了一个完整的教程,介绍了如何通过Anaconda安装tensorflow,并安装运行spyder编译器。文章详细介绍了安装Anaconda、创建tensorflow环境、安装GPU版本tensorflow、安装和运行Spyder编译器以及安装OpenCV等步骤。该教程适用于Windows 8操作系统,并提供了相关的网址供参考。通过本教程,读者可以轻松地安装和配置tensorflow环境,以及运行spyder编译器进行开发。 ... [详细]
  • vb6集成ad登录共享文件_SCSP实验2单点登录
    01—实验目的掌握单点登陆相关原理和深信服配置02—实验环境1.AC版本v12.0.42AC1地址:https:172.172.1.1AC2地址:htt ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 原文地址:https:www.cnblogs.combaoyipSpringBoot_YML.html1.在springboot中,有两种配置文件,一种 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 关于如何快速定义自己的数据集,可以参考我的前一篇文章PyTorch中快速加载自定义数据(入门)_晨曦473的博客-CSDN博客刚开始学习P ... [详细]
  • 1.修改CommonSettings.props文件下compute_xx,sm_xx,其中 ... [详细]
  • 初始化初始化本地空版本库,仓库,英文名repositorymkdirtest&&cdtestgitinit克隆项目到本地gitclone远程同 ... [详细]
  • Matlab 中的一些小技巧(2)
    1.Ctrl+D打开子程序  在MATLAB的Editor中,将输入光标放到一个子程序名称中间,然后按Ctrl+D可以打开该子函数的m文件。当然这个子程序要在路径列表中(或在当前工作路径中)。实际上 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
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社区 版权所有