近收到后台有用户问Haida是否会做折返滤镜?
关于折返滤镜,小编特地询问了一下职业摄影师以及技术人员,今天跟大家来“揭秘”一下所谓的折返滤镜。
1.折返滤镜,定义有失专业性
其实“折返”这个词并不是近期才出来的。追根溯源,很多接触摄影比较久的人可能会有所了解,它最早运用的就是大家所熟知的——“折返镜头”。
那么,什么是折返镜头呢?
光线经过两次反射,形成了三路光线,后组镜片是中心带孔的凹面反射镜,焦点处的凹面镜是比较小的反射镜,这个小凹面镜对于前端入射光线来说是不透明的,因为这个原因,导致折返镜头拍摄出的画面在焦外形成一个个环状的弥散圈、类似“甜甜圈”样的光斑。
(图源来自百度百科)
>简单地说,折返镜头就是利用光线的反射制造的镜头。正因这个镜头的原理是利用光线在镜头内折返,所以它叫折返镜头。折返镜最大的特点是拍摄出来的画面焦外会产生甜甜圈的效果。
使用折返镜成像时,景深外的高光点,在画面上不像通常镜头成像那样变成光斑,而是呈现出一个个小圆圈,营造出一种梦幻的效果。因此早些年,深受大家的喜爱。
但由于折返镜头受体积的影响,无法改变光圈(通常只有一档光圈),且无法实现自动对焦,需要使用者手动对焦,且画质也会差一些。因此,使用的人逐渐变少。
不过,还是有人喜欢它产生的特殊“甜甜圈”效果,最近市面上开始出现“折返滤镜”,模拟出甜甜圈效果的相关产品。
首先,滤镜是滤镜,顾名思义,是起到过滤光线的作用。其次,折返镜头之所以叫折返镜头是因为光线在镜头内发生折返,而使用所谓的“折返滤镜”并不会让入射光路发生反射现象。
因此这实则是一种不太专业的叫法,甚至有一丝玩弄概念的味道。
2.是否值得购买“甜甜圈”效果呢?
我们需要明确的是,能拍摄甜甜圈效果所需要的条件。
画面要产生甜甜圈或者其他光斑的重要条件是,必须要有明亮的点光源,且在焦外,所以使用时要注意:光斑的大小、明显与否均与此有关。
划重点!要达到甜甜圈效果,并不是我使用了某款产品就可以直接拍摄出来的,而是需要在特殊的场景下,才能实现,它对拍摄环境有一定的要求。(一般场景拍出来的图肉眼看起来并无差别)
以往,折返镜头一般是焦段500mm以上,多用于拍摄荷花或打鸟等,而现在更多是拍摄人像(小姐姐较多),利用自然光线、霓虹灯或者城市夜景灯,拍摄出迷离梦幻的效果。
那么为了实现这些这个梦幻效果,我们是否需要,或者说有必要专门购买一款产品呢?小编觉得没有太大必要。
花费成本高,产品的使用率相比于其他普通滤镜又低得多。如果能用更少的花费达到一样的效果,那对于热爱摄影的朋友来说,何乐而不为呢?
因此,我们为了能让更多人体验到“甜甜圈”的效果,教大家一个简易的制作方式,让你在家也能轻松做出能拍摄甜甜圈效果的工具~!
3.在家轻松做出“甜甜圈”特效
制作“甜甜圈”或者其他特殊光斑效果,其实很简单,只需要一张黑卡纸、一把剪刀、一支笔、双面胶即可。
如果对焦外的光斑形状比较在意,也可以用圆规、直尺等一些工具,来更好地画出自己所需的形状。需要焦外呈现什么形状,就用剪刀剪成什么形状就可以了。例如甜甜圈的圆形,五角星形、爱心形等等。
一般所要剪得图形直径大概在20-30mm之间,需要根据镜头口径大小做相应调整。大口径的镜头就使用稍大一些的圆形。剪得圆形太小或者太大会影响效果,所以上面那个区间,是小编尝试出的安全区间。
这次小编用的是佳能70-200mm的镜头,剪了20mm以及25mm的圆形,都拍摄成功了~
下面是这次简易制作“甜甜圈效果”的小视频,可为大家提供参考~!视频时长:1:14分
cript src="https://lf3-cdn-tos.bytescm.com/obj/cdn-static-resource/tt_player/tt.player.js?v=20160723">cript>
可能会有影友担心双面胶是否会对镜片的镀膜产生影响,小编在这里解答:镀膜质量合格的UV镜或者镜头保护镜,是完全不会有问题的~!
如果镀膜能被双面胶轻轻粘一下就粘掉了,那你可能买的是个假镜~!
以下为户外随手拍摄的效果图
< class="pgc-img">>< class="pgc-img">>想要大“甜甜圈”还是小圈,拍摄者可以根据自己的焦段以及光圈来任意改变,是不是很有意思呢?
还有小编随意剪的五角星形状,由于未使用直尺,仅纯手工绘画,所以五角星的形状并不是很好看,只是为大家演示一下效果,相信你们的手工肯定比手残的小编要好~
< class="pgc-img">>以上内容仅为大家科普一些摄影的小知识,希望让更多的人知道甜甜圈效果并没有想象的那么神奇~其实自己就可以动手完成,成就感满满~!
小编的1元成本构成:
1. 黑色卡纸:向我家萌娃借用
2. 圆规:向我家萌娃借用
3. 笔:向我家萌娃借用
4. 剪刀:向我家萌娃借用
5. 人工费:1.0元
以上1-5项合计成本1.0元。如果家里没小孩的影友,可向隔壁邻居小孩借用(方圆1里总会有小孩的)。
祝大家也能亲手实践成功~!
< class="pgc-img">>诱引力
风光纪实摄影师,中国摄影家协会会员、视觉中国签约摄影师、图虫认证摄影师、POCO网年度十佳生态摄影师、中国古建筑摄影大赛年度十佳,动感佳能摄影大赛生态类金奖。NiSi合作摄影师 腾龙合作摄影师。
一些资深的摄影爱好者都知道或者接触过折返镜头,折返镜头又称反射式镜头、反射远摄镜头。折返镜头有一个很显著的特点:在主体以外的焦外高光点上呈现出大小不一的空心圆形光斑,俗称“甜圈圈”,而不像普通镜头那样变成圆形或者椭圆形的光斑。在拍摄静物、人像或者生态题材时,可以形成别具一格的视觉效果,增加画面的趣味性和艺术感染力,运用得当时能营造作品梦幻的氛围感和朦胧的意境美。
500mm F8 折返镜头拍摄
500mm F8 折返镜头拍摄
由于折返头的这一个特性,往往被很多摄影人用于创作一些特殊的画面效果,但它也具有一些缺点。首先,折返镜头的锐度较差。其次,体积和重量较大,并且折返镜头只有一级光圈,即光圈值是固定的,无法通过调整光圈的大小来控制画面的景深。第三,折返镜头大多仅能通过手动对焦,在实际拍摄过程中,由于折返镜头本身焦段很长(大多在400MM以上),景深较浅,容易造成对焦难,对焦慢。最后,折返镜头大部分都是胶片时代相机及镜头厂家所生产,生产年代较早,目前市面上已经停产,只能通过二手市场进行选购,价格不菲(拍摄效果最佳的美能达250折返价格破万),质量良莠不齐。纠于以上原因,许多想追求“甜圈圈”的摄友不得不放弃这种有趣的拍摄方式。
500mm F8 折返镜头拍摄
NiSi公司2020年5月底推出的NiSi甜甜圈魔术镜,解决了朋友们想拍摄出“甜圈圈”的效果而又苦于没有折返镜头的烦恼。现在只要你手中拥有中长焦镜头如70-200mm,或大光圈定焦镜头如85mm/1.8,照样可以拍出与折返镜头相同效果的“甜圈圈”,焦距越长光圈越大效果越好。方法很简单,将NiSi甜甜圈魔术镜粘贴在普通的UV镜、CPL偏振镜或ND减光镜中央。这样,通过“价廉物美”的方式就可以将自己的大光圈长焦镜头,变身为一只能拍出“ 甜圈圈”的“折返”镜头,同时不影响镜头本身的自动对焦和可变焦及调整光圈大小的功能,并且保持原厂镜头的高画质。
NiSi甜甜圈魔术镜使用效果
安装成功后,与平时拍摄操作方式一样,光圈设定在F2.8-4效果明显。使用镜头的最长段,焦外的高光点便在画面上呈现出一个个小圆圈,形成可爱的同心圆光斑,层层叠叠。焦距越长光圈越大效果越明显。同时还要注意主体与背景之间的空间也要有较大的距离,焦外圈圈的效果才会明显。否则纵然使用了长焦和大光圈焦外形成的圈圈形状也会比较小,颜色比较淡。
100-400MM 400端 F5.6 使用前
100-400MM 400端 F5.6 使用后
100-400MM 400端 F5.6 使用前
100-400MM 400端 F5.6 使用心形魔术镜后
可以明显的看到使用NiSi甜甜圈魔术镜后,原来圆形光斑的位置被“甜甜圈”所替换了。
100-400MM 400端 F5.6 使用后
焦外的圈圈大且圆
100-400MM 288端 F5.6 使用后
焦外的圈圈密集但相对小
100-400MM 400端 F5.6 使用前
100-400MM 400端 F5.6 使用后
由于使用前反光光源不明显而且主体间隔背景距离不够远,所以圈圈效果不明显,但可以观察到焦外还是有圈圈的痕迹而且焦外较使用前有朦胧和梦幻的感觉。
100-400MM 400端 F5.6 使用后
使用后,主体间隔背景距离较上图远,圈圈的感觉更明显一些。
100-400MM 400端 F5.6 使用后
使用后,主体间隔背景距离较上图更远,且花瓣上的反光点突出,所以圈圈的感觉更加明显。
100-400MM 400端 F5.6
主体背景分离不够,圈圈不明显
100-400MM 400端 F5.6
主体背景分离较远,圈圈明显
使用技巧
NiSi甜甜圈魔术镜拍摄主体时,要尽量利用侧逆光、逆光,也就是说画面本身的焦外光斑比较明显的时刻,使用魔术镜则能将这些耀眼的光斑幻化成梦幻的“ 甜圈圈”,而清晨和黄昏由于光线色温的暖色调特性则能营造出金黄色的“ 甜圈圈”。夜晚也可以利用自然光线、霓虹灯或者城市夜景灯,拍摄出迷离梦幻的效果。
100-400MM 400端 F5.6
使用NiSi甜甜圈魔术镜后 ,侧逆光
100-400MM 400端 F5.6
使用NiSi甜甜圈魔术镜后,逆光拍摄
100-400MM 400端 F5.6
逆光,临近黄昏拍摄,焦外的甜甜圈呈暖色
100-400MM 400端 F5.6
逆光,临近黄昏拍摄,焦外的甜甜圈呈暖色
100-400MM 400端 F5.6
逆光,临近黄昏拍摄,焦外的甜甜圈呈暖色
100-400MM 400端 F5.6
夜光拍摄,NiSi甜甜心魔术镜
由于画面要产生光斑才能通过魔术镜转变为甜甜圈,所以创作过程中受天气的影响比较大,阴天很难出效果,而日光和夜光环境下容易出效果,拍摄的重要条件就是焦外必须要有明亮的点光源。所以选景时一定要注意焦外光斑的大小、多少与明显程度。
100-400MM 400端 F5.6
100-400MM 400端 F5.6
100-400MM 400端 F5.6
100-400MM 400端 F5.6
拍摄时要注意寻找和发现主体周边的光亮物体和林间树丛空隙形成的点光源,包括植物叶面上的反光或者水珠等反光光源。只有点光源和反光源处于焦外成像时,才会因拍摄距离与光点大小而形成美丽且大小不一的光环。所以,拍摄时不是任何主体以外的距离都能够形成圆圈圈,这就需要靠自己在取景过程中注意观察和实践,慢慢总结出一套适合自己的经验之谈。
100-400MM 400端 F5.6
水面的荷叶反光形成圈圈
100-400MM 400端 F5.6
水面的波光反射形成圈圈
100-400MM 400端 F5.6
水面的白鹭反光形成圈圈
100-400MM 400端 F5.6
水面的荷叶反光形成圈圈
100-400MM 400端 F5.6
水面的荷叶反光形成圈圈
100-400MM 400端 F5.6
竹林的间隙光源形成圈圈
100-400MM 400端 F5.6
逆光午后竹林的间隙光源形成圈圈
100-400MM 400端 F5.6
逆光树林叶间的空隙光源形成圈圈
100-400MM 400端 F5.6
逆光树林叶间的空隙光源形成圈圈
100-400MM 400端 F5.6
逆光树林叶间的空隙光源形成背景圈圈+前景中的叶面反光点光源形成圈圈 拍摄甜甜圈最好的效果就是前景和背景都有圈圈存在,将需要表现的主体包裹其中,最大化地形成梦幻的视觉氛围。
70-200MM 200端 F2.8 侧逆光效果
自制甜甜圈拍摄
70-200MM 200端 F2.8 逆光效果
自制甜甜圈拍摄
基于焦距越长光圈越大 “ 甜圈圈”效果越明显这一特性。拍摄过程中势必景深非常浅,稍有不注意,焦内主体就容易模糊和脱焦,所以对焦时一定要耐心细致,必要时使用三脚架和快门线。条件允许时,也可以调整到手动对焦,辅助实时放大对焦的功能,慢慢旋转对焦环,找到主体最为清晰的部分和最佳的构图位置再按动快门。要拍摄出”甜甜圈“”很容易,但要创作出焦内锐利,焦外梦幻的画意作品则需要耐心和观察力。
100-400MM 400端 F5.6 自制甜甜圈
如果相机带有多重曝光功能,或者熟悉后期处理,也可以利用多重曝光的技术来拍摄梦幻的画意效果。具体操作方式为,将焦内的锐利主体和焦外虚化的甜圈圈分开进行拍摄,可以拍摄一张清晰的主体和一张或多张梦幻的焦外背景,然后通过相机本身的叠加功能或者是电脑后期技术将主体与背景进行叠加。利用相机自带的二次曝光功能时,要注意构图的位置和曝光补偿的合理利用,利用电脑进行叠加相对创作的自由度较大,比较方便进行二次构图。
100-400MM 400端 F5.6 相机2次曝光
100-400MM 400端 F5.6 后期二次曝光
自制甜甜圈拍摄
100-400MM 400端 F5.6 相机2次曝光
自制甜甜圈拍摄
由于拍摄过程中,主体和背景的距离不够或者光线原因导致焦外形成圈圈的效果不佳时,一次成像很难达到需要的效果,就可先手动虚焦,拍摄焦外部分,具体虚焦程度以达到圈圈最圆最亮为佳,然后再拍静物、生态主体,拍摄时可以利用点测光或者背景加暗色背景布遮挡,最后再通过相机机内合成或者电脑叠加,效果就非常出彩了。
100-400MM 400端 F5.6 后期二次曝光
这是2次曝光所用的素材。
通过练习有一定经验后,我们还可以在平时拍摄时,刻意地对准林间树叶缝隙透出的点光源、波光粼粼的反射光源和夜晚斑驳的灯光光源,通过手动虚焦形成大小不一色彩斑斓的“甜圈圈”进行拍摄,做为背景素材,等碰到合适的主体和氛围时,将这些素材和主体进行叠加形成二次曝光的作品。
100-400MM 400端 F5.6 手动虚焦拍摄的素材
100-400MM 400端 F5.6 手动虚焦拍摄的素材 未使用
100-400MM 400端 F5.6
手动虚焦拍摄的素材,使用心形魔术镜
100-400MM 400端 F5.6
手动虚焦拍摄的素材,使用圆形魔术镜
100-400MM 200端 F5.6
手动虚焦拍摄的素材,使用圆形魔术镜
100-400MM 400端 F5.6 后期二次曝光
自制甜甜圈拍摄
另外令人惊喜的是NiSi这次推出了不同尺寸和心形的甜甜圈魔术镜,大大提高了我们在拍摄过程中的可玩性,我们可以通过大小和形状的选择来控制甜甜圈的大小和形态。
创作过程中的花絮:使用圆形魔术镜
100-400MM 400端 F5.6,使用心形甜甜圈魔术镜
100-400MM 400端 F5.6
手动虚焦,使用心形甜甜圈魔术镜
我相信拆卸方便和便于携带的特性,一定能使广大影友在外采风之际利用好它,创作出属于自己的精彩作品。
100-400MM 400端 F5.6,使用心形甜甜圈魔术镜
最后说一点,网上也有一些“DIY”甜甜圈的教程,指导大家自行动手制作然后通过胶布或者胶水黏贴在UV镜上使用,我自己也曾经制作过。但在使用过程中发现:使用的黑色贴纸或者其他遮挡物,由于材质不同,厚薄不一,透光率也不一样,加之边缘不够平滑和有毛刺,还有就是大小规格不统一,最后在拍摄时会造成甜甜圈的成像厚薄不够均匀,边缘不齐,甜甜圈不明显等一系列问题。另外,制作时需要用到黏贴材料,使用的UV镜也就基本只能用于模拟甜甜圈拍摄使用了,因为拆卸后容易弄脏、弄污UV镜,以后再用又要重新粘贴,比较麻烦。使用NiSi出品的甜甜圈魔术镜,可以在无损的情况下从滤镜上方便安装和拆卸,使用时安装到位,不用时拆下,这样你手中已有的滤镜可以“一举两得”,无需重复购买增加成本,也不影响在常规拍摄过程中的正常使用。
图文/万诱引力
编辑/Emily
<>< class="tt_format_content js_underline_content autoTypeSetting24psection " id="js_content">要:在三维渲染技术中,符号距离函数很难理解,而本文作者仅用 46 行 Python 代码就展示了这项技术。
链接:https://vgel.me/posts/donut/
声明:本文为 CSDN 翻译,未经允许禁止转载。
符号距离函数(Signed Distance Function,简称 SDF)是一种很酷的三维渲染技术——但不幸的是,这种技术很难理解。
该技术通常都通过 GLSL 编写的 shader 示例来展示,但大多数程序员并不熟悉 GLSL。从本质上来说,SDF 的编写思路非常简单。在本文中,我将编写一个程序来展示这项技术:一段只有 46 行 Python 代码的程序,使用 RayMarching 算法做出了一个甜甜圈动图。
当然,你也可以用其他自己喜欢的语言来编写这段代码,即便没有图形 API 的帮助,我们仅凭 ASCII 码也可以实现这样的动图。所以,一起来试试看吧!最终,我们将获得一个用 ASCII 码制作的、不停旋转的、美味的甜甜圈。只要掌握了这种渲染技术,你就可以制作各种动图。
准备工作
首先,我们用 Python 来渲染每一帧 ASCII。此外,我们还将添加一个循环来实现动画效果:
import time
def sample(x: int, y: int) -> str:
# draw an alternating checkboard pattern
if (x + y + int(time.time())) % 2:
return '#'
else:
return ' '
while True:
# loop over each position and sample a character
frame_chars=[]
for y in range(20):
for x in range(80):
frame_chars.append(sample(x, y))
frame_chars.append('\n')
# print out a control sequence to clear the terminal, then the frame
# (I haven't tested this on windows, but I believe it should work there,
# please get in touch if it doesn't)
print('3[2J' + ''.join(frame_chars))
# cap at 30fps
time.sleep(1/30)
以上代码为我们呈现了一个 80x20 的棋盘格,每秒交替一次:
这只是基础,下面我们来增加一些视觉效果——下一个任务很简单:决定屏幕上的每个字符显示什么。
画圆圈
首先,从最简单的工作着手。我们根据 x 坐标和 y 坐标绘制一个圆,虽然这还不是 3D 动画。我们可以通过几种不同的方式来画圆圈,此处我们采用的方法是:针对屏幕上的每个字符,决定应该显示什么,也就是说我们需要逐个字符处理。对于每个字符的坐标 (x, y),我们的基本算法如下:
1. 计算 (x, y) 到屏幕中心的距离:√((x-0)^2 + (y-0)^2),即 √(x^2+y^2)。
2. 减去圆半径。如果该点在圆的内部或边缘,则得到的值 ≤ 0,否则值 > 0。
3. 验证得到的值与 0 的关系,如果该点在圆的内部或边缘,则返回 #,否则返回空格。
此外,我们还需要将 x 和 y 分别映射到 -1..1 和 (-.5)..(.5) 上,这样结果就不会受分辨率的影响了,还可以保证正确的纵横比(2 * 20/ 80=0.5,因为 y 仅包含 20 个字符,而 x 包含 80 个字符,终端字符的高度大约是宽度的两倍)——这样可以防止我们画的圆圈看起来像一个压扁的豆子。
import math, time
def circle(x: float, y: float) -> float:
# since the range of x is -1..1, the circle's radius will be 40%,
# meaning the circle's diameter is 40% of the screen
radius=0.4
# calculate the distance from the center of the screen and subtract the
# radius, so d will be < 0 inside the circle, 0 on the edge, and > 0 outside
return math.sqrt(x**2 + y**2) - radius
def sample(x: float, y: float) -> str:
# return a '#' if we're inside the circle, and ' ' otherwise
if circle(x, y) <=0:
return '#'
else:
return ' '
while True:
frame_chars=[]
for y in range(20):
for x in range(80):
# remap to -1..1 range (for x)...
remapped_x=x / 80 * 2 - 1
# ...and corrected for aspect ratio range (for y)
remapped_y=(y / 20 * 2 - 1) * (2 * 20/80)
frame_chars.append(sample(remapped_x, remapped_y))
frame_chars.append('\n')
print('3[2J' + ''.join(frame_chars))
time.sleep(1/30)
我们成功地画出了一个圆!这个例子中并没有使用 time.time(),所以这个圆不会动,不过我们稍后会添加动画。
二维的甜甜圈
一个圆只是二维甜甜圈的一半,而另一半是中间的那个洞洞,也就是另一个圆。下面,我们在这个圆的中心加上一个洞,让它变成甜甜圈。实现方法有好几种,不过最好的方式是用一个半径和半径之外的厚度来定义:
import math, time
def donut_2d(x: float, y: float) -> float:
# same radius as before, though the donut will appear larger as
# half the thickness is outside this radius
radius=0.4
# how thick the donut will be
thickness=0.3
# take the abs of the circle calculation from before, subtracting
# `thickness / 2`. `abs(...)` will be 0 on the edge of the circle, and
# increase as you move away. therefore, `abs(...) - thickness / 2` will
# be ≤ 0 only `thickness / 2` units away from the circle's edge on either
# side, giving a donut with a total width of `thickness`
return abs(math.sqrt(x**2 + y**2) - radius) - thickness / 2
def sample(x: float, y: float) -> str:
if donut_2d(x, y) <=0:
return '#'
else:
return ' '
while True:
frame_chars=[]
for y in range(20):
for x in range(80):
remapped_x=x / 80 * 2 - 1
remapped_y=(y / 20 * 2 - 1) * (2 * 20/80)
frame_chars.append(sample(remapped_x, remapped_y))
frame_chars.append('\n')
print('3[2J' + ''.join(frame_chars))
time.sleep(1/30)
这种表示方法(半径 + 厚度)能呈现很好的艺术效果,因为半径和厚度是相对独立的参数,二者可以单独变更,几乎不需要互相参照。
这样代码也很好写,我们只需要稍微调整计算距离的方式:之前我们计算的是到圆心的距离,现在我们计算到圆边线的距离。边距 - 厚度/2,如果结果 ≤ 0,说明点到圆边线的距离 ≤ 厚度/2,这样就得到一个给定厚度的圆环,其圆心位于给定半径的圆的边线上。
另外一点好处是,代码的改动非常小:只需更新符号距离函数,而渲染循环的其余部分都不需要修改。也就是说,无论我们使用哪个 SDF,渲染循环都可以保持不变,我们只需要针对每个像素采样其距离即可。
三维模型
下面,我们来画三维模型。首先,我们来做一个简单的练习:渲染一个球体,它的 SDF 与圆几乎相同,只不过我们需要再加一个坐标轴 Z。
def sphere(x: float, y: float, z: float) -> float:
radius=0.4
return math.sqrt(x**2 + y**2 + z**2) - radius
此处,X 是水平轴,Y 是纵向轴,而 Z 表示深度。
我们还需要重用同一个 frame_chars 循环。唯一需要修改的函数是 sample,我们需要处理第三个维度。从根本上来说, sample 函数需要接受一个二维点(x, y),并以这个二维点为基础,在三维空间中采样。换句话说,我们需要“计算”出正确的 Z,以确保渲染正确的字符。我们可以偷懒采用 z=0,这样渲染出来的就是三维世界中的一个二维切片,即对象在平面 z=0 上的截面。
但为了渲染出更立体的三维视图,我们需要模拟现实世界。想象一只眼睛,它(近似)是一个二维平面,如何看到远处的物体呢?
太阳光有可能直接照射到眼睛所在的平面上,也有可能经过一个或多个物体反射后到达眼睛。我们可以按照同样的方式来渲染三维视图:每次调用 sample(x, y) 时,从一个模拟的光源射出无数光线,其中至少有一条光线经过物体反射后,穿过点 (x, y , camera_z)。但这种方法的速度会有点慢,某条光线照射到特定点的几率微乎其微,因此大部分工作都是无用功。要是这样写代码的话,用最强大的虚拟机也运行不完,所以我们来走一条捷径。
对于函数 sample(x, y),我们只关心穿过 (x, y, camera_z) 的光线,所以根本没必要在意其他光线。我们可以反向模拟光线:从 (x, y, camera_z) 出发,每走一步首先计算 SDF,获取光线从当前点到场景中任意一点(方向任意)的距离。
如果距离小于某个阈值,则意味着光线击中了场景中的点。反之,我们可以安全地将光线向前“移动”相应的距离,因为我们知道场景中有的点至少位于这段距离之外(实际情况可能更复杂,设想一下光线可能与场景足够接近,但永远不会进入场景:当光线接近场景时,SDF 的计算结果会非常小,所以光线只能更加缓慢地前进,但最终在足够多的次数之后,光线会越过场景中的点,然后加快前进速度)。我们将前进的最大步数限制为 30,如果届时光线没有击中任何场景中的点,则返回背景字符。综上所述,下面就是这个三维函数的定义:
def sample(x: float, y: float) -> str:
# start `z` far back from the scene, which is centered at 0, 0, 0,
# so nothing clips
z=-10
# we'll step at most 30 steps before assuming we missed the scene
for _step in range(30):
# get the distance, just like in 2D
d=sphere(x, y, z)
# test against 0.01, not 0: we're a little more forgiving with the distance
# in 3D for faster convergence
if d <=0.01:
# we hit the sphere!
return '#'
else:
# didn't hit anything yet, move the ray forward
# we can safely move forward by `d` without hitting anything since we know
# that's the distance to the scene
z +=d
# we didn't hit anything after 30 steps, return the background
return ' '
渲染球体的所有代码如下:
import math, time
def sphere(x: float, y: float, z: float) -> float:
radius=0.4
return math.sqrt(x**2 + y**2 + z**2) - radius
def sample(x: float, y: float) -> str:
radius=0.4
z=-10
for _step in range(30):
d=sphere(x, y, z)
if d <=0.01:
return '#'
else:
z +=d
return ' '
# this is unchanged
while True:
frame_chars=[]
for y in range(20):
for x in range(80):
remapped_x=x / 80 * 2 - 1
remapped_y=(y / 20 * 2 - 1) * (2 * 20/80)
frame_chars.append(sample(remapped_x, remapped_y))
frame_chars.append('\n')
print('3[2J' + ''.join(frame_chars))
time.sleep(1/30)
好了,我们绘制出了一个三维的球体。下面,我们来画三维的甜甜圈。
三维的甜甜圈
为了绘制三维的甜甜圈,接下来我们需要将绘制球体的 SDF 换成绘制更复杂的甜甜圈,其余代码保持不变:
import math, time
def donut(x: float, y: float, z: float) -> float:
radius=0.4
thickness=0.3
# first, we get the distance from the center and subtract the radius,
# just like the 2d donut.
# this value is the distance from the edge of the xy circle along a line
# drawn between [x, y, 0] and [0, 0, 0] (the center of the donut).
xy_d=math.sqrt(x**2 + y**2) - radius
# now we need to consider z, which, since we're evaluating the donut at
# [0, 0, 0], is the distance orthogonal (on the z axis) to that
# [x, y, 0]..[0, 0, 0] line.
# we can use these two values in the usual euclidean distance function to get
# the 3D version of our 2D donut "distance from edge" value.
d=math.sqrt(xy_d**2 + z**2)
# then, we subtract `thickness / 2` as before to get the signed distance,
# just like in 2D.
return d - thickness / 2
# unchanged from before, except for s/sphere/donut/g:
def sample(x: float, y: float) -> str:
z=-10
for _step in range(30):
d=donut(x, y, z)
if d <=0.01:
return '#'
else:
z +=d
return ' '
while True:
frame_chars=[]
for y in range(20):
for x in range(80):
remapped_x=x / 80 * 2 - 1
remapped_y=(y / 20 * 2 - 1) * (2 * 20/80)
frame_chars.append(sample(remapped_x, remapped_y))
frame_chars.append('\n')
print('3[2J' + ''.join(frame_chars))
time.sleep(1/30)
这个甜甜圈还不够完美,下面我们来添加一些动画,证明它是三维的。
旋转的三维甜甜圈
为了让甜甜圈旋转起来,我们需要在计算 SDF 之前对 sample 计算的点进行变换:
def sample(x: float, y: float) -> str:
...
for _step in range(30):
# calculate the angle based on time, to animate the donut spinning
θ=time.time() * 2
# rotate the input coordinates, which is equivalent to rotating the sdf
t_x=x * math.cos(θ) - z * math.sin(θ)
t_z=x * math.sin(θ) + z * math.cos(θ)
d=donut(t_x, y, t_z)
...
在这段代码中,y 值保持不变,所以甜甜圈是围绕 y 轴旋转的。我们在每次采样时计算 θ 值,然后计算旋转矩阵:
import math, time
def donut(x: float, y: float, z: float) -> float:
radius=0.4
thickness=0.3
return math.sqrt((math.sqrt(x**2 + y**2) - radius)**2 + z**2) - thickness / 2
def sample(x: float, y: float) -> str:
z=-10
for _step in range(30):
θ=time.time() * 2
t_x=x * math.cos(θ) - z * math.sin(θ)
t_z=x * math.sin(θ) + z * math.cos(θ)
d=donut(t_x, y, t_z)
if d <=0.01:
return '#'
else:
z +=d
return ' '
while True:
frame_chars=[]
for y in range(20):
for x in range(80):
remapped_x=x / 80 * 2 - 1
remapped_y=(y / 20 * 2 - 1) * (2 * 20/80)
frame_chars.append(sample(remapped_x, remapped_y))
frame_chars.append('\n')
print('3[2J' + ''.join(frame_chars))
time.sleep(1/30)
这样,三维的甜甜圈就画好了。下面,我们使用法向量估算器,添加一些简单的光照和纹理。
添加光照和糖霜
为了增加光照和糖霜纹理,我们需要计算法向量。法向量的定义是从对象表面上每个点垂直地发散出来的向量,就像仙人掌上的刺,或者某人在接触到静电气球后头发爆炸的样子。
大多数表面都有计算法向量的公式,但是当一个场景中融合了多个 SDF 时,计算就会非常困难。另外,谁愿意针对每个 SDF 编写一个法向量函数?所以,我们还是需要一点小技巧:在目标点周围的每个轴上对 SDF 进行采样,并以此来估算法向量:
Sdf=typing.Callable[[float, float, float], float]
def normal(sdf: Sdf, x: float, y: float, z: float) -> tuple[float, float, float]:
# an arbitrary small amount to offset around the point
ε=0.001
# calculate each axis independently
n_x=sdf(x + ε, y, z) - sdf(x - ε, y, z)
n_y=sdf(x, y + ε, z) - sdf(x, y - ε, z)
n_z=sdf(x, y, z + ε) - sdf(x, y, z - ε)
# normalize the result to length=1
norm=math.sqrt(n_x**2 + n_y**2 + n_z**2)
return (n_x / norm, n_y / norm, n_z / norm)
为了理解该函数的原理,我们可以假设一种特殊情况:法向量的一个分量为 0,比如 x=0。这意味着,这个点上的 SDF 在 x 轴上是平的,也就是说 sdf(x + ε, y, z)==sdf(x - ε, y, z)。
随着这些值的发散,法向量的 x 分量会向正方向或负方向移动。这只是一个估算值,但对于渲染来说已经足够了,甚至一些高级演示也会使用这种方法。但这种方法的缺点是速度非常慢,因为每次调用都需要对 SDF 进行六次采样。随着场景 SDF 变得越来越复杂,性能就会出现问题。
不过,对我们来说这就足够了。如果光线命中,我们就在 sample 中计算法向量,并使用它来计算一些光照和纹理:
if d <=0.01:
_, nt_y, nt_z=normal(donut, t_x, y, t_z)
is_lit=nt_y < -0.15
is_frosted=nt_z < -0.5
if is_frosted:
return '@' if is_lit else '#'
else:
return '=' if is_lit else '.'
我们只关心法向量的 y 和 z 分量,并不在意 x 分量。我们使用 y 来计算光照,假设表面朝上(法向量的 y 接近 -1),则应该被照亮。我们使用 z 来计算糖霜材质,针对不同的值设定阈值,就可以调整甜甜圈的糖霜厚度。为了理解这些值的含义,你可以试试看修改如下代码:
import math, time, typing
def donut(x: float, y: float, z: float) -> float:
radius=0.4
thickness=0.3
return math.sqrt((math.sqrt(x**2 + y**2) - radius)**2 + z**2) - thickness / 2
Sdf=typing.Callable[[float, float, float], float]
def normal(sdf: Sdf, x: float, y: float, z: float) -> tuple[float, float, float]:
ε=0.001
n_x=sdf(x + ε, y, z) - sdf(x - ε, y, z)
n_y=sdf(x, y + ε, z) - sdf(x, y - ε, z)
n_z=sdf(x, y, z + ε) - sdf(x, y, z - ε)
norm=math.sqrt(n_x**2 + n_y**2 + n_z**2)
return (n_x / norm, n_y / norm, n_z / norm)
def sample(x: float, y: float) -> str:
z=-10
for _step in range(30):
θ=time.time() * 2
t_x=x * math.cos(θ) - z * math.sin(θ)
t_z=x * math.sin(θ) + z * math.cos(θ)
d=donut(t_x, y, t_z)
if d <=0.01:
_, nt_y, nt_z=normal(donut, t_x, y, t_z)
is_lit=nt_y < -0.15
is_frosted=nt_z < -0.5
if is_frosted:
return '@' if is_lit else '#'
else:
return '=' if is_lit else '.'
else:
z +=d
return ' '
while True:
frame_chars=[]
for y in range(20):
for x in range(80):
remapped_x=x / 80 * 2 - 1
remapped_y=(y / 20 * 2 - 1) * (2 * 20/80)
frame_chars.append(sample(remapped_x, remapped_y))
frame_chars.append('\n')
print('3[2J' + ''.join(frame_chars))
time.sleep(1/30)
到这里,我们的三维甜甜圈就画好了,不仅做了光照和纹理处理,还可以不停地旋转,而且只用到了 46 行代码!