Bingo, Computer Graphics & Game Developer
本文源于Unreal官方Rendering Overview的基本概念理解和翻译
与Unreal Engine 3中的Forward Lighting Path不同的是,Unreal Engine 4中所有的光源都采用的是延迟渲染的技术。各种材质都会讲各自的属性写入GBuffers当中,因此在根据光源计算时,光源直接通过Buffer上的per-pixel的材质属性来计算光照。
Forward Lighting Path本质上就是最传统的光源场景渲染方式,二者对比可见Forward Rendering vs. Deferred Rendering。当在指定渲染目标时,前者的做法是对场景中的每一个几何体都对每个光源进行一次管线中的光照计算,最后直接着色叠加。
后者为先行计算四张2D Image/Buffer(Screen Space Depth,Surface Normals,Diffuse Color,Specular color and specular power),VS中计算各图所需的几何体信息,而PS只存图,之后每个光源才与四张图上的内容进行着色计算。
总结而言 Forward Lighting Path的伪代码
for each object
for each light
shading
Deffered Shading
for each pixel
for each light
shading
Forward Rendering Path
Deffered Shading
不过延迟渲染也不是完美的,在比较小的场景下延迟渲染会明显增加显存消耗和渲染速度下降,所以比较适合于较大型场景。另一个缺点是,延迟渲染对待多着色模型的场景是比较吃力。(感谢孤独的守候的知识分享)
UE4中有三种不同的光源
这些光源的使用场景也是根据不同的游戏场景,不同的性能品质要求会需要的权衡。每个游戏都还为自己选用最合适的光源类型。
Movable Lights: 在运行期间可以改变位置,旋转朝向,改变颜色,亮度,衰退因子,半径等任何光源拥有的属性都可以动态修改。因此这些光源不会预先烘焙光照贴图,也不会产生任何的间接光照(实时渲染中间接光照一般都是预计算完成)。
Static Lights: 静止光源的位置是不可以改变的,但是光源的亮度和颜色等都可以随之改变,因此这也是与静态光源有着最大差异的地方。但是其所改变的光源的属性都只会影响直接光照,间接光照都是由Lightmass预计算的,因此运行时改变其值也不会影响。
Stationary lights:静态光源是不能在运行时改变任何属性的光源,他们会在Lightmass中计算完毕,之后不会对性能造成任何过多的影响。因此他是三者当中拥有中等效果,较低的可更改性但也是性能最友好的一个光源类型。
以及
半透明材质的发光以及着色是在单一的光源前进传递(Single Forward Pass)中保证其正确性的,但是在多通道的光源下并不适用的技术。
半透明材质可以向非透明物体上投射阴影,也可以向自身或者其他半透明材质上投射阴影(如图所示)
Jade Material
Ice Material
次表面散射着色模型是为了用于模拟蜡烛,玉制物体而做的新着色模型。他们并不是完全的半透明,但会在内部散射入射进来的光。这会比实现皮肤渲染上质量稍低但开销更小。
UE4支持在GPU上模拟粒子效果,传统的CPU难以完成的上千种粒子效果可以在GPU内更快效果更好的实现。
在GPU粒子中最有趣的一项特性莫过于向量场了,一个向量场是在一个网格中均匀影响所在其中粒子运动的一个向量集,向量场在UE4中是作为一个Actors来出现的,因此他本身也可以像其他Actors一样被位移,旋转,或者是缩放,所以在运行期间他们都是可以被动态移动的。
例如一个向量场可以被放置在瀑布上,当粒子进入了向量场的边界时,他们都会被其所影响,当粒子群离开了边界其作用的效果也会淡去(fade out)。
环境光遮蔽(Ambient Occlusion): 目前使用的环境光遮蔽为SSAO(屏幕空间环境光遮蔽,Screen Space Ambient Occlusion),只会依赖与深度缓存。所以也不会被细节法线贴图或者Smoothing groups影响。因此在一些面元较少的多边形上开启这个效果时,就会显得非常生硬。所以UE4只会在选项AmbientCubemap上开启这个效果。
环境光立方体贴图(Ambient Cubemaps): 该效果对整个场景中的光照采用一立方体贴图,所以这个效果也是与当前材质所在位置是互相独立的。贴图的计算需要用到观察者的位置,材质的粗糙度(镜面反射效果),以及当前材质的面法线。这也就带来了高质量高效的环境光实现。
镜头高光(Bloom): 高光是真实世界光照的现象,较低的渲染开销却可以带来非常好的感官真实度的提升。高光效果一般可以在非常暗的背景中裸眼观察非常亮的物体观测到。
人眼适应:可以会自动调整场景的曝光度,重现从明亮环境进入黑暗环境(或相反)时所经历的效果。
镜头眩光:会在镜头转向明亮物体时自动产生镜头的眩光效果。
色调映射(Tone Mapping):使得渲染场景的颜色可以被转换或修改来得到不同的效果。这可以被用来制作诸如棕褐滤镜,或击中特效(例如红色闪光)等。
此篇的内容为观看Ray Tracing Course多遍后并结合部分PBRT原文的笔记内容
Radiant Flux
之所以不使用辐射通量的原因是因为我们无法分辨究竟:「大量的能量穿过了一个小面积区域」or 「较少的能量穿过了一个大面积区域」(这里能量的多少指代平均面积上能量的分布)
Irradiance
又称入射辐射面密度
不使用辐照度的原因是,无法分辨:「大量能量的入射角较大」or「少量能量的入射角较小」情况
Radiance
sr表示单位角,在更高纬度的情况而言,又称为solid angle,单位为立体弧度
本质上BRDF就是用来描述「有多少从方向入射的光从方向上反射出去的函数」
BRDF个人觉得可以多层次解读,首先,BRDF讲解了入射光和反射光的概率密度分布,因为他本质上只是一个入射辐射度和出射辐射度的比例(也就是返回值);另一个角度而言,在给定了入射方向时,BRDF也负责计算出射方向的概率。
菲涅尔公式的作用是计算入射光时,发生反射和折射的概率计算,也可以被认为是入射光能量分布情况,而斯奈尔法则则是确定入射光之后的折射光和反射光角度计算
Snell’s Law中展示出的全反射现象,只会出现在光从光疏介质到光密介质的情况下下。
面法线的计算本质上就是求解给定曲面指定点上的梯度,给定双曲面,其中为给定的双曲面的曲率,因此给定点求解面法线,就是所求梯度。
对于焦比的科普可以参考A Simple Explanation of F-Stop
本质上从光学上理解,景深的就是因为快门的开关之间进光量的多少造成的。因此例如视频中讲解的,实际单反的之类的写法本质上就是用于给出光圈大小。
但因为同样的光圈大小也可能因为焦距不同导致进光量有所不同,因此不直接给出的具体值,而是给出类似之类的写法。
而进光量越大,通过透镜的光子也就越多,非焦距上的物体边缘也就显得越模糊,换一种说法就是用于描述非焦距上的物体的光子也就越多。
X代表在上的N个随机采样,记为
因此本质上使用无意识的统计规律算法,对与原始的进行变换(可见PBRT)
最佳的重要性采样(最小的方差),就是函数上的每一个采样点的采样概率,都与其函数值成比例即可()
因此一个较佳的本质上就是与被积函数成比例的函数,如若选择一个均匀概率分布函数,那么就是非重要性采样和重要性采样之间的差距了
例如 ,简化渲染方程的计算(BRDF采用最简单的漫反射模型.
配图为康奈尔盒
所有的内容都是根据Ray Tracing Course上的课程所做的笔记, 因此会包含单词释义, 概念记录等等
单词 | 释义 |
---|---|
Radiant flux | 辐射通量 |
Irradiance | 辐射度 |
Radiance | 辐射 |
3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11
10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63
-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1
-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59
H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359
c-16-25.333-24-45-24-59z’/> | 从观察者方向发出的方向 |
| | 面法线 |
| | 指向光源的向量 |
| | 全局光照中反射而来的光线方向 也就是光源在表面上L反射出的间接光照 , 也就是得到 乘以 表示在方向上的投影 在根据平行四边形法则即可得到 |
| | 入射角 |
| | 反射角 |
不同的材质在接受入射光时, 会反射向不同的方向以及吸收不同数量的光
(入射光方向, 光在表面上的点, 出射光方向)
因此称他们的集合为BSDF或者为BxDF 也就是说一个表面既能反射也能折射的现象
双向性: 也就是fr(\omega’, x, \omega) = fr(\omega, x, \omega’) 当出射光变为入射光再次计算 结果得到原来的入射光方向
正性: BRDF的fr(\omega’, x, \omega) ≥0
能量守恒: 表面可能会吸收或者反射入射光 但是两者的能量和都不能超过入射光能量
在所有可能的出射(折射 反射的面上积分) 同时也算上了光的衰减现象
(实际为入射光和出射光能量的比值) \le1 如果=1那么就是发生了全反射现象 \le1也就是说一部分的光被吸收了 不可能出现能量不守恒的情况
Rendering Equation
单词 | 释义 |
---|---|
从表面点x上离开的辐射度 | |
从表面点x上发射出的辐射度 | |
3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11
10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63
-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1
-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59
H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359
c-16-25.333-24-45-24-59z’/> | 所有外来的入射光引起的出射光的总和 |
| ) | 从给定方向上指向点x的入射辐射度 |
| | BRDF |
| | 光衰减系数 |
最简单的BRDF模型 ambient模型 尽管不是基于物理 但会是一个很好理解的上手模型公式 也就是一个模型将会有不受位置角度影响的基本RGB颜色
单词 | 释义 |
---|---|
I | 光强度 |
环境光系数 | |
环境光光照强度 |
漫反射Diffuse模型
单词 | 释义 |
---|---|
漫反射系数 | |
3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11
10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63
-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1
-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59
H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359
c-16-25.333-24-45-24-59z’/> | 指向光源的向量 |
| | 面法线 |
镜面反射模型Specular
单词 | 释义 |
---|---|
镜面系数 | |
3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11
10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63
-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1
-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59
H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359
c-16-25.333-24-45-24-59z’/> | 指向观察者/摄像机的方向 |
| | 反射光线的方向 |
| | 反光度 |
Phong反射模型就是讲上述三者模型集中在一起的一个最简化的模型
这也就是为什么无论你怎么样变换摄像机的位置, 也就是的方向, 但是漫反射颜色都不会因此发生改变的原因。而镜面反射同理, 是因为一部分高光从表面上反射而来, 也就是与我们的观察方向之间的夹角非常的接近, 这也就使得最终的显示在最终的效果上显得非常明亮的原因。
最终的受过大量简化后的Rendering Equation如下
这只发生在直接光照上, 而间接光照没有考虑在其中, 在光线照射不到的地方, 环境光帮助我们找回了看不见的区域。
注意 这是一个基于物理的渲染的近似 只用于方便理解而做
单词 | 释义 |
---|---|
3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11
10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63
-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1
-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59
H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359
c-16-25.333-24-45-24-59z’/>(θ) | 入射角为时发生反射的可能性 |
| | 当入射角为0的时候, 也就是发现方向, 发生反射的概率,也可以是, 为媒介的传输系数 在真空或者空气下为1 |
因此在反射现象不明显折射现象较为明显的玻璃这些材质时, 最终计算出来的将会非常的接近0
原始的Fresnel equation
因此当传输的介质为真空时, Schlick的近似公式计算出来的只是非常贴近0的, 而原始公式计算出的可能性直接就是0
全反射现象
计算出的结果有一定的可能出现大于1的情况
也就是光从介质系数大的物体中折射到小系数中, 折射光线与法线夹角成90度, 此时折射现象就不再发生。
折射光线那么这时就发生了全反射现象此时
Ray的参数表示形式 , 一般的平面圆的隐式方程表示为, 例如球的一般方程为, 而其参数方程就可以写成
球的方程
Ray的方程
简单的直线方程代入球面坐标求解即可
如若给定一个表面的隐式方程, 那么他的面法线就是该函数在处的梯度, 梯度定义为
例如求椭球形面的面法线
其中a和b为椭球形的曲率
在求得光线与最近物体交点的时候, 我们会排除的情况, 因为时属于自相交情况
一个光源下的阴影计算方式
因此我们可以通过发射shadow ray来近似上述计算结果
在本质上这就是Monte Carlo Integration
新增的为递归项
单词 | 释义 |
---|---|
菲涅尔透射系数(发生折射的概率) | |
透射方向来的光强度 | |
菲涅尔反射系数(发生反射的概率) | |
反射方向来的光强度 | |
任何反射而来的漫反射光照 | |
D] |
可以看到的就是illumination Equation中使用的I本质上是物理空间中不存在的光照强度, 而不是Global illumination中的辐射度计算。
Ray Tracing 中无法做到以下几点
因为在ray tracing当中我们只计算了局部的incoming漫反射光照, 但事实上我们应该采集在整个上半球上的所有入射光, 也就是渲染方程中积分的意义
真实的基于物理的漫反射BRDF模型为, 代表反射率, , 并且下列BRDF也是概率分布函数, 只需要检验.
Diffuse BRDF
在这个模型中, 它将会收集整个半球上的入射光, 那么如果手动射入一根光线, 那么出射光出射在半球上的任意方向上得概率都是一致的。其中, 该BRDF函数也是一个概率分布函数。
镜面反射模型也就意味着如若给定一个入射光方向, 那么出射光的方向只有一个。
Specular BRDF
简单来讲就是一个二项分布
Hit or Miss法
在一张纸上给定的区域内绘制当前的函数
向整个区域打点
记录每个点的Hit/Miss情况
最终结果就可以这样近似
同理也就可以估计的值
如果场景中只有一个面积非常小的点光源, 那么即便是spp足够大, 最终的结果也很有可能是一团黑
原因在于, 只有当发射的ray相交到了一个光源时, 他才会返回一个足够的光亮度, 因此每次在BRDF的概率密度范围内随机反射光线, 只有很小的概率击中光源并返回一个值。
解决方案, 极大的缩短追踪Depth是最终的目标。因此在每次递归到Surface上一点时, 不仅仅只是计算上从随机方向上反射而来得光照, 还需直接计算点到光源之间的通路(间接等价于, 随机发射一条光线到光源方向并返回一个可能的值)。
这就大大的缩短了Path Tracing中需要Sample的光线的数量。同样数量的光线的情况下, 整个场景会显得格外的明亮。
需要注意的是, 如若在Path Tracing中随机hit到了光源, 我们并不直接返回光源的亮度, 因为我们在Path Tracing的过程中已经计算过他的亮度。这也就很好的解耦了直接光照和间接光照。
优点:
当构建一个渲染时, 你不会得到真正的随机采样结果, 因此在渲染一个动画的时候, 你能够得到连贯一致的噪点分布。
缺点:
没有……
问题出在, 渲染方程本质上是一个无穷积分, 因此理论上来说Path Tracing的Depth应该是无穷大, 但事实上不能。因此什么时候结束tracing就成为了一个比较大的问题。
因此我们的目标就是, 使用概率论来解决这个问题, 当完成了第i次迭代之后, 是否停止继续追踪取决于概率, 在一定的概率内随机决定是否要终止当前这条光线。
假定目前有一随机变量
若, 那么继续追踪当前的光线, 最终计算得到的辐射度就为
若, 那么结束当前的光线追踪迭代, 最终计算得到的辐射度为0
可以通过计算期望, 来印证计算结果的正确性。
的取值范围, 这样的情况不会发生, 因为光线一次也不于物体相交(非法); 这样 情况也不会发生, 因为光线追踪永不停止。尽管的取值理论上在这区间内都是可以接受的, 但事实上我们最好选取一个能够让结果更快收敛的概率值。
所以的取值也就非常的关键, 我们总是希望更远的采样那些明亮的路径而不是较为黑不溜秋的路径, 因此我们就可以设为简单的反射率, 这可以极大的降低方差。
一致性算法表示算法本身在经过无穷的随机采样逼近之后, 将会收敛, 但这将会花上非常多的时间。
Bidirectional Path Tracing
遇到了光源在Next Event Estimation也难以处理的情况, 比如光源和摄像机之间存在一层遮罩, 因此只有摄像机发出的光光线随机的走到了光源中才能够返回一个较为明亮的颜色。
关键思想: 从摄像机和光源处同时发射光线, 并且在一定的depth结束彼此的追踪之后, 连接两条路径的结尾点即可。
总结:更快的焦散和计算收敛速度, 尤其是在室内场景中, 但添加上Multiple Importance Sampling和路径的权重似乎会增加计算时间。
Metropolis Light Transport
关键思想: 对于明亮的区域我们会采样更多的次数, 相比于暗处的区域的话。本质上讲就是明亮处采样的概率会更高。
配图为康奈尔盒
专业课没有开编译原理,因此刚刚上手Flex+Bison的时候有很多概念性错误。这里不做教程,只是我个人在学习中的心得
基于Flex与Bison学习
2015.10.13备注
2015.10.30备注
###Chapter 9
1.纯词法分析器和纯语法分析器协同工作。不仅是Flex上给出%option reentrant和%option bison-bridge+Bison %pure-parser那么简单(原本使用书上注明的%define api.pure在OSX上不可用,应该是版本问题,具体情况可以参考Webkit上的.flex)。
其中bison-bridge的作用就是完成Flex上与Bison上不协调的yylex()所做的对接。
// Flex
token = yylex(YYSTYPE *yylvalp);
// Bison
token = yylex(yyscan_t scaninfo);
之前自己在Flex中或Bison或具体外部使用之前的前向声明都可以隐藏,或手动声明为
int yylex(YYSTYPE *lvalp, yyscan_t scaninfo);
2.%parse-param/#define YYPARSE_PARAM和%lex-param/#define YYLEX_PARAM都可以声明传递给分析器应用数据的类型,但根据版本迭代的情况而言,以及借鉴Webkit使用情况,推荐使用%parse-param和%lex-param。
3.强烈建议包含Flex+Bison生成的头文件时查看包含关系。因为一旦重复包含或少包含都会引起非常难以调试的符号未定义BUG,哪怕在生成的源文件中已经#define了此符号。
4.Bison在定义%token时,使用到了某单符号,例如’-‘’+’等等,此类token若未在Bison中给出相应名,那么就需要分配其指定类型。
5.BNF范式在实现中,务必手工生成一遍语法树,很有可能其 空 的部分顺序是错误的,因为Bison在面对二义性语法上会选择匹配长度更长的,或者是最新匹配的那一部分,因此 空 实现的顺序就尤为关键。
6.Flex+Bison支持简单的class内置,所有的中文资料显示2009年时两者未能有效支持C++,需要%language “C++”来完成对C++的支持,目前看来无需此关键字也能完成class对struct的替换。
7.Flex中yyextra指针分配给指定对象的声明位置尤为关键不在头部的%{ %}对中,而是在词法分析单元声明部分中。
在使用编译器前端Flex+Bison完成了大部分工作之后,尽管可以使用简易的printf来完成终端调试,但终究不够方便。
使用Bison完成AST生成之后需要对其构建的AST进行分析,此时常规思路的前序中序后序遍历对这样一个庞大的二叉树而言实现可视重建并不容易。因为你需要通过前中序来手工推导出一整棵树,但这并不是你做此任务的主要目的。
这里我安利一波贝尔实验室推出的黑科技Graphviz,在原有的遍历基础上基本上能够做到1分钟完成上手。
假定一抽象语法树节点类Node,之后所有AST都将被视为普通二叉树。
简单的类定义
class Node
{
public:
Node *leftChild;
Node *rightChild;
// 访问当前结点
virtual void value();
};
传统的中序遍历
void traversal(Node * root)
{
if(!root) return;
traversal(root->leftChild);
root->value();
traversal(root->rightChild);
}
Graphviz支持DOT语言的输入,个人认为只要依样画葫芦即可
使用DOT输出的中序遍历 只需要增加当前结点的label名及各子结点地址 无论是printf或是保存文件都可选 目的无非就是获取这一整段DOT输出代码
class Node
{
public:
Node *leftChild;
Node *rightChild;
// 访问当前结点
virtual void value();
string name;
};
// 以下代码将会使得遍历转换为DOT格式
void traversal(Node * root)
{
// 可选将渲染的结点形状更换为其他形式
printf("digraph G {\n");
printf("node[shape=rect]\n");
traversalAsDot(root);
printf("}");
}
void traversalAsDot(Node* root)
{
if(!root) return;
// 当前结点名
// root->name.c_str() 可以更换为任意能够获取当前结点名的函数 手工RTTI都可
printf("_%p[label=%s]\n", root, root->name.c_str());
// 传递父节点到子节点地址信息
if (l != NULL) printf("_%p -> _%p\n", root, root->leftChild);
if (r != NULL) printf("_%p -> _%p\n", root, root->rightChild);
traversalAsDot(l);
root->value();
traversalAsDot(r);
}
我在这里是对CSS进行解析生成AST,解析以下代码,获得AST,输出DOT使用Graphviz渲染
head.font-face.a .b:hover, #ahs{
background: #00FF00;
src: 222;
}
.body {
font-family: 'Raleway', Arial, sans-serif;
margin: 1, 2, 3, 4;
}
输出的DOT如下
digraph G {
node[shape=rect]
_0059C010[label=RuleList]
_0059C010 -> _0059BE48
_0059C010 -> _0059BFF8
_0059BE48[label=RuleList]
_0059BE48 -> _0059BE30
_0059BE30[label=Ruleset]
_0059BE30 -> _0059BD70
_0059BE30 -> _0059BE18
_0059BD70[label=SelectorList]
_0059BD70 -> _00597EC0
_0059BD70 -> _0059BD58
_00597EC0[label=SelectorList]
_00597EC0 -> _00597E78
_00597E78[label=Selector]
_00597E78 -> _00597E30
_00597E78 -> _00597C98
_00597E30[label=SimpleSelector]
_00597E30 -> _00597DF0
_00597DF0[label=SpecifierList]
_00597DF0 -> _00597DD8
_00597DF0 -> _00597D50
_00597DD8[label=SpecifierPseudo]
_00597D50[label=SpecifierList]
_00597D50 -> _00597D38
_00597D38[label=SpecifierClass]
_00597C98[label=Selector]
_00597C98 -> _00597C50
_00597C50[label=SimpleSelector]
_00597C50 -> _00597C10
_00597C10[label=SpecifierList]
_00597C10 -> _00597BF8
_00597C10 -> _00597B70
_00597BF8[label=SpecifierClass]
_00597B70[label=SpecifierList]
_00597B70 -> _00597B58
_00597B58[label=SpecifierClass]
_0059BD58[label=Selector]
_0059BD58 -> _0059BD40
_0059BD40[label=SimpleSelector]
_0059BD40 -> _0059BCA0
_0059BCA0[label=SpecifierList]
_0059BCA0 -> _00597F68
_00597F68[label=SpecifierElement]
_0059BE18[label=DeclarationList]
_0059BE18 -> _0059BDD0
_0059BE18 -> _0059BE00
_0059BDD0[label=DeclarationList]
_0059BDD0 -> _0059BDA0
_0059BDA0[label=Declaration]
_0059BDA0 -> _0059BD88
_0059BD88[label=Expression]
_0059BD88 -> _0059C5C8
_0059C5C8[label=TermHex]
_0059BE00[label=Declaration]
_0059BE00 -> _0059BDE8
_0059BDE8[label=Expression]
_0059BDE8 -> _0059C620
_0059C620[label=TermInteger]
_0059BFF8[label=Ruleset]
_0059BFF8 -> _0059BED8
_0059BFF8 -> _0059BFE0
_0059BED8[label=SelectorList]
_0059BED8 -> _0059BEC0
_0059BEC0[label=Selector]
_0059BEC0 -> _0059BEA8
_0059BEA8[label=Selector]
_0059BEA8 -> _0059BE90
_0059BE90[label=SimpleSelector]
_0059BE90 -> _0059BE78
_0059BE78[label=SpecifierList]
_0059BE78 -> _0059BE60
_0059BE60[label=SpecifierClass]
_0059BFE0[label=DeclarationList]
_0059BFE0 -> _0059BF50
_0059BFE0 -> _0059BFC8
_0059BF50[label=DeclarationList]
_0059BF50 -> _0059BF38
_0059BF38[label=Declaration]
_0059BF38 -> _0059BF20
_0059BF20[label=Expression]
_0059BF20 -> _0059BF08
_0059BF20 -> _0059C8E0
_0059BF08[label=Expression]
_0059BF08 -> _0059BEF0
_0059BF08 -> _0059C860
_0059BEF0[label=Expression]
_0059BEF0 -> _0059C7E0
_0059C7E0[label=TermString]
_0059C860[label=TermIndent]
_0059C8E0[label=TermIndent]
_0059BFC8[label=Declaration]
_0059BFC8 -> _0059BFB0
_0059BFB0[label=Expression]
_0059BFB0 -> _0059BF98
_0059BFB0 -> _0059CA80
_0059BF98[label=Expression]
_0059BF98 -> _0059BF80
_0059BF98 -> _0059CA20
_0059BF80[label=Expression]
_0059BF80 -> _0059BF68
_0059BF80 -> _0059C9C0
_0059BF68[label=Expression]
_0059BF68 -> _0059C980
_0059C980[label=TermInteger]
_0059C9C0[label=TermInteger]
_0059CA20[label=TermInteger]
_0059CA80[label=TermInteger]
}
渲染结果为
假定已求解出交点,最后一步还需要将距离最短的交点筛选出来,有一个特殊情况如下图
实际交点应该是A,因此要剔除B的情况。比较简单,令交点到射线起始点向量为P,那么和射线方向向量d点积,看\( \mathtt{cos \theta} \)的符号即可(-90, 90)。
联立方程组无脑求解
在没有查阅之前,用的就是最基本的直线和圆方程强行联立求解,根据delta值来判断交点情况,最后根据交点与射线起始点的向量夹角筛选合适交点。
class ray
{
public:
// ...
// 0 = ax + by + c
float a, b, c;
}
强行联立
float k = -r.a / r.b, t = -r.c / r.b;
float A = k * k + 1.0f;
float B = 2 * k * t - 2 * c->y * k - 2 * c->x;
float C = c->x * c->x + t * t - 2 * c->y * t + c->y * c->y - c->radius * c->radius;
float delta = B * B - 4 * A * C;
求解 delta为0,正负任意
float x1 = (-B + sqrt(delta)) / (2.0f * A);
float y1 = k*x1 + t;
int x2 = (-B - sqrt(delta)) / (2.0f * A);
int y2 = k*x2 + t;
但是上的上述方法有一个缺陷,就是你必须讨论斜率k为无穷的情况。
向量求解
设圆心坐标为C
令圆形方程为\( \mathtt{(X-C) \ldotp (X-C)=r^2} \),向量点积自己就是求解自身长度的平方,射线方程令为\( \mathtt{R(t) = P + td, t \ge 0} \)。
直接代入\( \mathtt{(P+td-C) \ldotp (P+td-C)=r^2} \), 令\( \mathtt{P-C = m} \)
展开下式
等价于二次方程求根
基本b, c, t分量代式
这里直接用向量,免去了讨论斜率的情况。交点Q就是\( \mathtt{P + td} \)。