在Unity中渲染一个黑洞

前言

N年前观看《星际穿越》时,被其中的“卡冈图雅”黑洞所震撼。制作团队表示这是一个最贴近实际的黑洞效果,因为它是通过各种科学理论实现的。当时就想自己也做一个差不多的出来,无奈技术太菜。现在以掉了一堆头发为代价,终于实现出来了,分享给大家。这是最终效果:

本项目使用Unity 2018.4.23f1制作,完整项目请移步GitHub:https://github.com/RenChiyu/UnityBlackHole

转载请注明出处:https://www.cnblogs.com/GuyaWeiren/p/15376286.html

基础概念

从某度查询资料得知,目前理论上将黑洞分为如下四种类型:

  1. 史瓦西黑洞(没有电荷,不旋转)
  2. R-N黑洞(有电荷,不旋转)
  3. 克尔黑洞(没有电荷,旋转)
  4. 克尔-纽曼黑洞(有电荷,旋转)

这里我们以史瓦西黑洞为目的进行实现。因为它没有自旋且不带电荷,所以实现起来(比如套公式时)会比较方便。

一个黑洞如图所示,可以简单地视作三个部分:

1. 奇点

奇点是视觉上黑洞的中心部分,它是一个质量非常大,而密度趋近无限大的结构。

2. 事件视界

事件视界点简单理解就是,以黑洞的奇点为中心,第二宇宙速度小于光速的区域。从外部来看,事件视界内部的物体因为逃逸速度大于光速,导致光无法从该区域射出,因此在视界外的观测者眼中呈现一片黑色。这个区域可以视作一个黑色的球。

3. 吸积盘

吸积盘是物体向奇点跌落的过程中,物体由于奇点的强大引力造成的摩擦和压缩所释放出的电磁波辐射。吸积盘中的物质通常是高温气体,围绕着黑洞做高速旋转。它看起来像一个会发出明亮光线的盘。

除开以上三个,根据广义相对论,质量会使空间发生扭曲。光线经过这个扭曲空间时发生的偏移现象称之为引力透镜现象。质量越大,扭曲越严重,黑洞的质量必然会使空间发生明显的扭曲,这也就是为什么“卡冈图雅”看上去有一个两个星环(一个水平,一个垂直)的原因,其中垂直的星环就是水平星环被透镜扭曲形成的虚像。引力透镜可以让观测者看到被大质量天体遮挡的光源,从下图可以大概看出引力透镜的作用:

实现思路

在Unity中,光是沿直线传播的,没有办法转弯。《星际穿越》的特效团队为此特意打造了一套渲染引擎来实现它。对我们来说,如此高成本的活当然是duck不必的,需要采用另一种思路:光线步进法。

和引擎的渲染不同,光线步进的原理是反向操作致敬韦神:从摄像机经过每一个像素往外发射一个点,不断延长直到接触到的东西,再将碰撞处的颜色显示在对应像素上。这个过程是可以被我们的代码控制的,因此我们可以通过控制步进的总长度和每次步进的方向来反向实现扭曲的光。屏幕就像画布,而每一个检测点就是画笔。

因此,我们需要知道光线是怎么扭曲的。

公式推导

由于光的路径不是因重力而扭曲,这里不能简单用牛顿第二定律描述,而应当使用爱因斯坦引力场方程:

\[G_{\mu v}=R_{\mu v}-\frac{1}{2}g_{\mu v}R=\frac{8\pi G}{c^4}T_{\mu v}
\]

这是一个二阶非线性偏微分方程,直接求解非常困难。我们模拟史瓦西黑洞,可以使用方程的一个特殊解:史瓦西度规。它表示扭曲只取决于质量,忽略自旋和电荷:

\[\mathrm{d}s^{2}=c^{2}\left(1-{\frac{2GM}{c^{2}r}}\right)\mathrm{d}t^{2}-\left(1-{\frac{2GM}{c^{2}r}}\right)^{-1}\mathrm{d}r^{2}-r^{2}\mathrm{d}\Omega^{2}
\]

令\(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}\)。于是有:

\[ds^2 = \left(1-\frac{1}{r}\right)dt^2-\left(1-\frac{1}{r}\right)^{-1}dr^2-r^2d\varphi^2
\]

其中,\(r\)、\(t\)和\(\varphi\)都是史瓦西坐标系下的参数。

现在有了描述扭曲空间的方程,还需要一个方程用于描述光子在其中的运动轨迹。得到轨迹就能微分得到用于计算光线步进的方向方程。测地线方程用于描述在空间中两点之间的最短路径,完全符合需求,因此我们要将史瓦西度规套入测地线方程中。

测地线方程一般形式为:

\[\frac{dU^\mu}{d\lambda}+\Gamma_{\alpha\beta}^{\mu}U^\alpha U^\beta=0
\]

然后提取史瓦西度规中的两个守恒量:

  1. \(L=r^2\frac{d\varphi}{d\lambda}\)
  2. \(E=\left(1 -\frac{1}{r}\right)\frac{dt}{d\lambda}\)

对于测地线方程,\(L\)为角动量,\(E\)为系统能量。

光子的运动是类光世界线,有\(g_{\mu\nu}U^\mu U^\nu=0\),于是有:

\[\left(\frac{dr}{d\lambda}\right)^2=E^2-\frac{L^2}{r^2}\left(1-\frac{1}{r}\right)
\]

这样可以用\(E\)消去等式中的仿射参量\(\lambda\)。同时令\(u=\frac{1}{r}\),能得到:

\[\left(\frac{{du}}{{d\varphi}}\right)^2=\frac{E^2}{L^2}-u^2(1-u)
\]

由于\(E\)和\(L\)都是常量,于是两边对\(\varphi\)求导,能得到:

\[\left(\frac{d^2u}{d\varphi^2}\right)=u+\frac{3}{2}u^3
\]

注意到上式和比耐公式非常相似:

\[\left(\frac{d^2u}{d\varphi^2}\right)+u=-\frac{\mathbf{F}(u)}{m h^2u^2}
\]

上式中的\(\mathbf{F}\)是粒子受到的向心力,就是我们需要的结果。\(m\)是粒子的质量,令\(m=1\),最终可以得到:

\[\mathbf{F}(r) = -\frac{3}{2}h^2 \frac{r}{r^5}
\]

这个公式表示,奇点坐标为\((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);
}

由于靠近观察者的吸积盘颜色需要盖在事件视界上,所以不能直接返回黑色。

绘制吸积盘

吸积盘也没什么别的,大概三个要素:

  1. 一个旋转的圆形
  2. 越靠近奇点吸积盘的温度越高,也就是更加明亮
  3. 云状纹理

如果说还有一点那就是吸积盘的纹理。没有纹理,吸积盘光溜溜,一点也不真实。云状噪声图很适合作为吸积盘纹理。在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中渲染一个黑洞的更多相关文章

  1. 用体渲染的方法在Unity中渲染云(18/4/4更新)

    github: https://github.com/yangrc1234/VolumeCloud 更新的内容在底部 最近在知乎上看到一篇文章讲云层的渲染(https://zhuanlan.zhihu ...

  2. Unity中Instantiate一个prefab时需要注意的问题

    在调用Instantiate()方法使用prefab创建对象时,接收Instantiate()方法返回值的变量类型必须和声明prefab变量的类型一致,否则接收变量的值会为null.   比如说,我在 ...

  3. Unity中Instantiate一个prefab时需要注意的问题

    在调用Instantiate()方法使用prefab创建对象时,接收Instantiate()方法返回值的变量类型必须和声明prefab变量的类型一致,否则接收变量的值会为null.   比如说,我在 ...

  4. unity中把一个图片切割成两个UI图片

    1.在unity3D的Project视图下选中需要更改的图片,将图片的Texture Type更改为Sprite (2D and UI),点击Apply即可.操作如图所示: 2.完成步骤一,点击App ...

  5. unity中生成一个GUI格子(始终居中)

    1.Script程序 using UnityEngine; using System.Collections; public class GUITest : MonoBehaviour { [Seri ...

  6. 在Unity中高效工作(上)

    原地址:http://www.unity蛮牛.com/thread-19974-1-1.html 编的话:感谢做编程的IT朋友,帮我翻译文章,我又稍稍做了些修改.给点儿掌声哩.欢迎大家多多评论呦. 我 ...

  7. Unity Shader入门精要学习笔记 - 第6章 开始 Unity 中的基础光照

    转自冯乐乐的<Unity Shader入门精要> 通常来讲,我们要模拟真实的光照环境来生成一张图像,需要考虑3种物理现象. 首先,光线从光源中被发射出来. 然后,光线和场景中的一些物体相交 ...

  8. Unity中加入Android项目的Build步骤

    转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 简介: 有的项目需要在Android中加入Unity功能,例如ANDROID应用中嵌入Un ...

  9. unity 中UGUI制作滚动条视图效果(按钮)

    1.在unity中创建一个Image作为滚动条视图的背景: 2.在Image下创建一个空物体,在空物体下创建unity自带的Scroll View组件: 3.对滑动条视图的子物体进行调整: 4.添加滚 ...

随机推荐

  1. java字符串(String和StringBuilder)

    1.String 1.1.创建String对象的方法(三种方式) String s1 = "zhang"; 创建一个字符串对象zhang,名为s1 String s2 = new ...

  2. Javascript - Vue - webpack + vue-cil

    cnpm(node package manager)和webpack模块 npm是运行在node.js环境下的包管理工具(先安装node.js,再通过命令 npm install npm -g 安装n ...

  3. vue 优化hash持久化缓存

    公司用的是vue最近在学习react的打包时发现react会额外生成一个runtimeChunk,不知道具体原因所以查资料学习了下, 这里是runtime的功能,文章地址:https://sebast ...

  4. opengl中标准矩形像素点手动网格化为三角形条带的实现

    这里以一张矩形图片为例进行说明: 一张图片的像素点是孤立的,导入opengl中进行绘制出来,看起来没问题,但是当我们放大图片时候,显示的就是一个个孤立的点,而没有像看图软件放大图片那样看起来还是连续的 ...

  5. 微服务架构及raft协议

    微服务架构全景图 服务注册和发现 Client side implement 调用需要维护所有调用服务的地址 有一定的技术难度,需要rpc框架支持 Server side implement 架构简单 ...

  6. noip模拟48

    A. Lighthouse 很明显的容斥题,组合式与上上场 \(t2\) 一模一样 注意判环时长度为 \(n\) 的环是合法的 B. Miner 题意实际上是要求偶拉路 对于一个有多个奇数点的联通块, ...

  7. AgileConfig轻量级配置中心1.4.0发布,重构了发布功能

    加入 NCC 先说一个事,AgileConfig 在 7 月底终于通过了 NCC 社区的审核,正式成为了 NCC 大家庭的一员.这对 AgileConfig 来说是一个里程碑,希望加入 NCC 后能更 ...

  8. 通过HttpURLConnection下载图片到本地--批量下载

    一.背景说明 这篇文章讲述的是批量下载附件,在上一篇文章中,介绍了下载单个附件(上一篇文章). 二.实现思路 主要的实现思路:创建文件夹->文件夹中创建需要下载的文件->压缩文件夹-> ...

  9. 常见shell脚本测试题 for/while语句

    1.计算从1到100所有整数的和2.提示用户输入一个小于100的整数,并计算从1到该数之间所有整数的和3.求从1到100所有整数的偶数和.奇数和4.执行脚本输入用户名,若该用户存在,输出提示该用户已存 ...

  10. 跨域分布式系统单点登录的实现(CAS单点登录)

    1. 概述 上一次我们聊了一下<使用Redis实现分布式会话>,原理就是使用 客户端Cookie + Redis 的方式来验证用户是否登录. 如果分布式系统中,只是对Tomcat做了负载均 ...