在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.添加滚 ...
随机推荐
- Mysql---mysqldump参数详细说明(转)
Mysqldump参数大全(参数来源于mysql5.5.19源码) mysqldump.exe一般会默认安装在C:\Program Files\MySQL\MySQL Server 5.5\bin 参 ...
- SpringCloud之网关zuul
1.微服务网关介绍和使用场景 1)什么是网关 API Gateway,是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能 提供路由请求.鉴权.监控.缓存.限流等功能 统一接入 智 ...
- 移动端常用单位——rem
移动端常用单位: ①px:像素大小,固定值 ②%:百分比 ③em(不常用,但是在首行缩进时可以使用):相对自身的font大小(当自身的字体大小也是em做单位时,才会以父元素的字体大小为基准单位) ④r ...
- php常用的函数
addslashes: 字符串加入斜线.bin2hex: 二进位转成十六进位.chop: 去除连续空白.chr: 返回序数值的字符.chunk_split: 将字符串分成小段.convert_cyr_ ...
- SpringBoot笔记(4)
一.请求处理 1.1 常用参数注解使用 注解 使用 @PathVariable 获取URI模板指定请求,并赋值到变量中,不指定可以将所有请求放到map中,但是健值都为String @RequestHe ...
- RHEL7.2系统下的软件管理(yum)、本地yum源和网络yum源的搭建
在Liunx系统中,rpm和yum都可以安装软件,但rpm存在安装软件的依赖性,yum安装软件需要yum源 1.yum yum install softwarename ##安装 yum repoli ...
- TDSQL(MySQL版)之DB组件升级
随着数据库产品的更新迭代,修复bug等等,产品避免不了会出现升级的需求.TDSQL(MysqL版)也会有这方面的需求.接下来我就说说如何对现有TDSQL(MySQL版)集群组件进行升级,而不影响业务. ...
- Linux制作根文件系统笔记
测试平台 宿主机平台:Ubuntu 12.04.4 LTS 目标机:Easy-ARM IMX283 目标机内核:Linux 2.6.35.3 交叉编译器:arm-linux-gcc 4.4.4 Bus ...
- 浅析mybatis中${}和#{}取值区别
mybatis作为一个轻量级的ORM框架,应用广泛,其上手使用也比较简单:一个成熟的框架,必然有精巧的设计,值得学习. 在使用mybatis框架时,在sql语句中获取传入的参数有如下两种方式: ${p ...
- VSCode一些设置
//每次保存后自动格式化 "editor.formatOnSave": true, // #每次保存的时候将代码按eslint格式进行修复 "editor.codeAct ...