在Unity中渲染一个黑洞
在Unity中渲染一个黑洞
前言
N年前观看《星际穿越》时,被其中的“卡冈图雅”黑洞所震撼。制作团队表示这是一个最贴近实际的黑洞效果,因为它是通过各种科学理论实现的。当时就想自己也做一个差不多的出来,无奈技术太菜。现在以掉了一堆头发为代价,终于实现出来了,分享给大家。这是最终效果:
本项目使用Unity 2018.4.23f1制作,完整项目请移步GitHub:https://github.com/RenChiyu/UnityBlackHole
转载请注明出处:https://www.cnblogs.com/GuyaWeiren/p/15376286.html
基础概念
从某度查询资料得知,目前理论上将黑洞分为如下四种类型:
- 史瓦西黑洞(没有电荷,不旋转)
- R-N黑洞(有电荷,不旋转)
- 克尔黑洞(没有电荷,旋转)
- 克尔-纽曼黑洞(有电荷,旋转)
这里我们以史瓦西黑洞为目的进行实现。因为它没有自旋且不带电荷,所以实现起来(比如套公式时)会比较方便。
一个黑洞如图所示,可以简单地视作三个部分:
1. 奇点
奇点是视觉上黑洞的中心部分,它是一个质量非常大,而密度趋近无限大的结构。
2. 事件视界
事件视界点简单理解就是,以黑洞的奇点为中心,第二宇宙速度小于光速的区域。从外部来看,事件视界内部的物体因为逃逸速度大于光速,导致光无法从该区域射出,因此在视界外的观测者眼中呈现一片黑色。这个区域可以视作一个黑色的球。
3. 吸积盘
吸积盘是物体向奇点跌落的过程中,物体由于奇点的强大引力造成的摩擦和压缩所释放出的电磁波辐射。吸积盘中的物质通常是高温气体,围绕着黑洞做高速旋转。它看起来像一个会发出明亮光线的盘。
除开以上三个,根据广义相对论,质量会使空间发生扭曲。光线经过这个扭曲空间时发生的偏移现象称之为引力透镜现象。质量越大,扭曲越严重,黑洞的质量必然会使空间发生明显的扭曲,这也就是为什么“卡冈图雅”看上去有一个两个星环(一个水平,一个垂直)的原因,其中垂直的星环就是水平星环被透镜扭曲形成的虚像。引力透镜可以让观测者看到被大质量天体遮挡的光源,从下图可以大概看出引力透镜的作用:
实现思路
在Unity中,光是沿直线传播的,没有办法转弯。《星际穿越》的特效团队为此特意打造了一套渲染引擎来实现它。对我们来说,如此高成本的活当然是duck不必的,需要采用另一种思路:光线步进法。
和引擎的渲染不同,光线步进的原理是反向操作致敬韦神:从摄像机经过每一个像素往外发射一个点,不断延长直到接触到的东西,再将碰撞处的颜色显示在对应像素上。这个过程是可以被我们的代码控制的,因此我们可以通过控制步进的总长度和每次步进的方向来反向实现扭曲的光。屏幕就像画布,而每一个检测点就是画笔。
因此,我们需要知道光线是怎么扭曲的。
公式推导
由于光的路径不是因重力而扭曲,这里不能简单用牛顿第二定律描述,而应当使用爱因斯坦引力场方程:
\]
这是一个二阶非线性偏微分方程,直接求解非常困难。我们模拟史瓦西黑洞,可以使用方程的一个特殊解:史瓦西度规。它表示扭曲只取决于质量,忽略自旋和电荷:
\]
令\(c=1\),设史瓦西半径(即黑洞的事件视界半径)\(r_s=\frac {2GM}{c^2}=1\),再引入球极坐标,即\(\mathrm{d}\Omega^2=\mathrm{d}\theta^2+\sin^2\theta \mathrm{d}\varphi^2\)。由于史瓦西黑洞附近的空间是球对称的,还可以令\(\theta=\frac{\pi}{2}\)。于是有:
\]
其中,\(r\)、\(t\)和\(\varphi\)都是史瓦西坐标系下的参数。
现在有了描述扭曲空间的方程,还需要一个方程用于描述光子在其中的运动轨迹。得到轨迹就能微分得到用于计算光线步进的方向方程。测地线方程用于描述在空间中两点之间的最短路径,完全符合需求,因此我们要将史瓦西度规套入测地线方程中。
测地线方程一般形式为:
\]
然后提取史瓦西度规中的两个守恒量:
- \(L=r^2\frac{d\varphi}{d\lambda}\)
- \(E=\left(1 -\frac{1}{r}\right)\frac{dt}{d\lambda}\)
对于测地线方程,\(L\)为角动量,\(E\)为系统能量。
光子的运动是类光世界线,有\(g_{\mu\nu}U^\mu U^\nu=0\),于是有:
\]
这样可以用\(E\)消去等式中的仿射参量\(\lambda\)。同时令\(u=\frac{1}{r}\),能得到:
\]
由于\(E\)和\(L\)都是常量,于是两边对\(\varphi\)求导,能得到:
\]
注意到上式和比耐公式非常相似:
\]
上式中的\(\mathbf{F}\)是粒子受到的向心力,就是我们需要的结果。\(m\)是粒子的质量,令\(m=1\),最终可以得到:
\]
这个公式表示,奇点坐标为\((0, 0, 0)\)时,坐标在\(r(x, y, z)\)所受到的加速度。其中,\(h=r^2\frac{\mathrm{d}\theta}{\mathrm{d}t}\)是粒子的角动量。
渲染实现
得到了最关键的公式,接下来就是奥利给干啦兄弟们!
SDF简介
在开始敲代码前,先介绍一下后面会用到的SDF。它的全称是Signed Distance Field,中文名为有向距离场。SDF函数描述了一个图形的区域,我们习惯性地设置它的规则是点在图形内部则返回负值,点在图形外部返回正值。在光线步进法中,利用各种SDF函数可以绘制出不同的图形。如下是一个以原点为中心点,半径为1的球体的SDF函数:
// @param pPosition 需要判定的点
fixed sdfSphere(fixed3 pPosition)
{
return length(pPosition) - 1;
}
在这里可以找到更多图形的SDF函数:https://iquilezles.org/www/articles/distfunctions/distfunctions.htm
准备资源
准备一个天空盒的Cubemap
,创建两个C#脚本
、Shader
和材质球
。
- 第一个脚本需要挂在
Camera
上做后处理 - 第二个脚本用于鼠标控制
Camera
角度和坐标,方便从各个方向观察渲染结果
我们在像素着色器中对每一个像素往外发射一道光线,最终碰撞到天空盒上:
struct appdata
{
fixed4 vertex : POSITION;
fixed2 uv : TEXCOORD0;
};
struct v2f
{
fixed4 vertex : SV_POSITION;
fixed3 rayDir : TEXCOORD0;
};
v2f vert (appdata i)
{
v2f o;
o.vertex = UnityObjectToClipPos(i.vertex);
// 变换得到屏幕四个角向外的射线
fixed3 dir = mul(unity_CameraInvProjection, fixed4(i.uv * 2.0f - 1.0f, 0.0f, -1.0f));
o.rayDir = normalize(mul(unity_CameraToWorld, fixed4(dir, 0.0f)));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
const fixed step = 0.1; // 步进长度,太大会有横纹
fixed3 pos = _WorldSpaceCameraPos;
fixed3 dir = i.rayDir * step;
fixed4 color = fixed4(0, 0, 0, 1);
UNITY_LOOP
for (int i = 0; i < 300; i++)
{
// 步进
pos += dir;
}
// 天空盒
fixed4 skyBox = texCUBE(_SkyBoxTex, dir);
color.rgb += DecodeHDR(skyBox, _SkyBoxTex_HDR).rgb;
return color;
}
如果没有问题,在运行起来后能看到天空盒。
绘制事件视界
这个非常简单,直接使用球的SDF:
// 事件视界
if (eventHorizon(pos)) < 0)
{
return fixed4(color, 1);
}
由于靠近观察者的吸积盘颜色需要盖在事件视界上,所以不能直接返回黑色。
绘制吸积盘
吸积盘也没什么别的,大概三个要素:
- 一个旋转的圆形
- 越靠近奇点吸积盘的温度越高,也就是更加明亮
- 云状纹理
如果说还有一点那就是吸积盘的纹理。没有纹理,吸积盘光溜溜,一点也不真实。云状噪声图很适合作为吸积盘纹理。在Photoshop中使用分层云彩可以快速制作出一个噪声图。
于是可以编写吸积盘的绘制代码:
fixed3 accretionDisk(fixed3 pPosition)
{
const fixed MIN_WIDTH = 2.6; // 由于引力透镜,事件视界看起来是没有引力透镜的2.6倍
fixed r = length(pPosition);
fixed3 disk = fixed3(_AccretionDiskWidth, 0.1, _AccretionDiskWidth); // 视作一个压扁的球
if (length(pPosition / disk) > 1)
{
return fixed3(0, 0, 0);
}
fixed temperature = max(0, 1 - length(pPosition / disk));
temperature *= (r - MIN_WIDTH) / (_AccretionDiskWidth - MIN_WIDTH);
// 坐标转换为球极坐标系
fixed t = atan2(pPosition.z, pPosition.x); // θ
fixed p = asin(pPosition.y / r); // φ
fixed3 sphericalCoord = fixed3(r, t, p);
fixed noise = 0;
// 使用两层噪声叠加出云的纹理
UNITY_LOOP
for (int i = 1; i < 4; i++)
{
fixed2 noiseUV;
fixed speedFactor;
if(i % 2 == 0) // 云和环状效果
{
noiseUV = sphericalCoord.xy;
speedFactor = 1;
}
else
{
noiseUV = sphericalCoord.xz;
speedFactor = -1;
}
noise += tex2D(_AccretionDiskTex, noiseUV * pow(i, 3)).r;
sphericalCoord.y += _AccretionDiskSpeed * _Time.x * speedFactor;
}
// 橙红色作为吸积盘颜色
fixed3 color = fixed3(1, 0.5, 0.4);
return temperature * noise * color * _AccretionDiskBright;
}
绘制引力透镜效果
根据上文推算出的公式,直接计算出步进方向偏移量叠加上去:
fixed3 gravitationalLensing(fixed pH2, fixed3 pPosition)
{
fixed r2 = dot(pPosition, pPosition);
fixed r5 = pow(r2, 2.5);
return -1.5 * pH2 * pPosition / r5;
}
fixed3 h = cross(pos, dir);
fixed h2 = dot(h, h);
// ...
for (int i = 0; i < 300; i++)
{
// ...
// 引力透镜
fixed3 offset = gravitationalLensing(h2, pos);
dir += offset;
pos += dir;
}
这样就完成了黑洞和吸积盘的渲染。运行起来,调整一下摄像机角度,可以看到:
后续处理
加上抗锯齿柔化硬边,再加上Bloom让明亮处更加柔和,调整一下摄像机的位置和角度就OJBK了。也可以根据喜好加上其他的后处理调色。这是我调出的最终效果:
Bloom没有使用AssetStore中的,因为都特么要收费。放上我使用的链接
后记
有一种丰收的喜悦,做完之后非常开心,浑身充满了力量。
很惭愧,就做了一点微小的工作,谢谢大家。
在Unity中渲染一个黑洞的更多相关文章
- 用体渲染的方法在Unity中渲染云(18/4/4更新)
github: https://github.com/yangrc1234/VolumeCloud 更新的内容在底部 最近在知乎上看到一篇文章讲云层的渲染(https://zhuanlan.zhihu ...
- Unity中Instantiate一个prefab时需要注意的问题
在调用Instantiate()方法使用prefab创建对象时,接收Instantiate()方法返回值的变量类型必须和声明prefab变量的类型一致,否则接收变量的值会为null. 比如说,我在 ...
- Unity中Instantiate一个prefab时需要注意的问题
在调用Instantiate()方法使用prefab创建对象时,接收Instantiate()方法返回值的变量类型必须和声明prefab变量的类型一致,否则接收变量的值会为null. 比如说,我在 ...
- unity中把一个图片切割成两个UI图片
1.在unity3D的Project视图下选中需要更改的图片,将图片的Texture Type更改为Sprite (2D and UI),点击Apply即可.操作如图所示: 2.完成步骤一,点击App ...
- unity中生成一个GUI格子(始终居中)
1.Script程序 using UnityEngine; using System.Collections; public class GUITest : MonoBehaviour { [Seri ...
- 在Unity中高效工作(上)
原地址:http://www.unity蛮牛.com/thread-19974-1-1.html 编的话:感谢做编程的IT朋友,帮我翻译文章,我又稍稍做了些修改.给点儿掌声哩.欢迎大家多多评论呦. 我 ...
- Unity Shader入门精要学习笔记 - 第6章 开始 Unity 中的基础光照
转自冯乐乐的<Unity Shader入门精要> 通常来讲,我们要模拟真实的光照环境来生成一张图像,需要考虑3种物理现象. 首先,光线从光源中被发射出来. 然后,光线和场景中的一些物体相交 ...
- Unity中加入Android项目的Build步骤
转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 简介: 有的项目需要在Android中加入Unity功能,例如ANDROID应用中嵌入Un ...
- unity 中UGUI制作滚动条视图效果(按钮)
1.在unity中创建一个Image作为滚动条视图的背景: 2.在Image下创建一个空物体,在空物体下创建unity自带的Scroll View组件: 3.对滑动条视图的子物体进行调整: 4.添加滚 ...
随机推荐
- [ASP.NET MVC]@RenderSection,@RenderBody(),@RenderPage
1.@RenderBody() 作用和母版页中的服务器控件类似,当创建基于此布局页面的视图时,视图的内容会和布局页面合并,而新创建视图的内容会通过布局页面的@RenderBody()方法呈现在标签之 ...
- Windows上使用Python2.7安装pip
资料包含: setuptools-33.1.1 pip-18.1 ez_setup.py get-pip.py 方法一 先安装 setuptools: 进入 setuptools 文件夹,运行:pyt ...
- ASP截取字符 截取字符之间的字符
ASP截取字符:MID函数Mid(变量或字串符,开始字节, 结尾字节(可不填)) InStrRev(变量, "字串符") 最后出现位置InStr(变量, "字串符&qu ...
- 786. 第k个数
题目传送门 一.理解感悟 1.这是快速排序模板的练习题. 2.不一样的地方在于它可以利用快排模板,但却不需要真的把所有数据排序完成,每次一分为二后,只关心自己所有的那一半,就是可以节约一半的递归. 3 ...
- golang操作etcd
etcd是近几年比较火热的一个开源的.分布式的键值对数据存储系统,提供共享配置.服务的注册和发现,本文主要介绍etcd的安装和使用. etcd介绍 etcd是使用Go语言开发的一个开源的.高可用的分布 ...
- 字符串截取子串(Java substring , indexOf)
前言 因为之前java课设做的是股票分析系统,我找的接口返回的是一个.csv文件,因为这种文件里面的数据是以逗号分隔的,所以要对数据进行分析的时候需要截取子串,并且以逗号作为截取的标志.所以接下来就说 ...
- Python之struct模块
面对网络协议,在组包拆包时,python提供了struct模块,它可以帮助我们在python值和C语言的结构体之间相互转换,下面一起来了解struct的具体用法. 假设,我们的网络协议为消息id(un ...
- 用XPath定位Web页面元素时,如何快速验证XPath语句是否正确?
在使用Selenium做Web UI自动化测试的过程中,XPath是一种定位页面元素的常用方式.然而,面对某些元素的XPath路径过于复杂,我们想快速验证拼凑的Xpath语句是否正确时,该怎么办呢?这 ...
- 在node节点部署kubectl管理k8s集群
感谢!原文链接:https://blog.csdn.net/sinat_35930259/article/details/79994078 kubectl是k8s的客户端程序,也是k8s的命令行工具, ...
- string类型数据的操作指令
1. 2. 3. 4. 5. 6. 7. 8. 9. 从右到左是索引从-1开始 10. 11. 12. 13. 14. 15.