Catlike学习笔记(1.3)-使用Unity画更复杂的3D函数图像
第三篇来了~今天去参加了 Unite 2018 Berlin,感觉就是。。。。非常困。。。回来以后稍微睡了下清醒了觉得是时候认真学习下了,不过讲的很多东西都是还没有发布或者只有 Preview 的版本,按照 Unity 的习惯肯定 Bug 多到令人发指,最近不太想折腾所以就先继续写文章把。。按照惯例奉上『原文链接』
PART 1 概述
首先大概介绍一下什么是『Catlike教程』,大家自行访问一下就会发现是这位『大神』写的一个 Unity 系列教程,里面由浅至深的以一个个有趣的小课题来引导大家学习 Unity 的方方面面~回想自己毕业三年都在做 Unity 游戏开发,然而看了大神的教程以后发现自己欠缺的东西非常多~真正对引擎的掌握程度非常低只是在不停的拼 UI 写业务逻辑。做这个系列呢也是希望自己可以坚持把大神的教程学完让自己变得更厉害~就酱。。
那么言归正传我们本期节目的最终目标是实现作者配图中的看起来很屌的图形,像是这样的。。。
对比上一篇文章的函数图像,大概有以下几个关键点需要实现。
- 支持多函数叠加
- 从一条曲线变成一个曲面
- 由曲面扩展成真正的三维图形
PART 2 支持多函数叠加
首先我们的目标是可以通过一个滑杆来控制「上一篇」中的曲线显示的函数,因此先复制之前的代码改改名字比如 Graph3DController.cs 再修改类名与文件名一致。然后我们的关键是需要修改这一行
var pos = new Vector3(x, Calc(x), 0);
使其变成根据滑杆中的 int 值选择 delegate 中的某个函数,如下所示,代码中主要修改的地方用注释稍微解释了下。
// 新的 deleagate
public delegate float Function(float x, float t);
// 记得修改类名与文件名一致否则不能挂在 gameobject 上
public class Graph3DController : MonoBehaviour
{
[Range(10, 100), SerializeField] private int _resolution;
[SerializeField] private GameObject _cube;
// 添加新的滑杆
[Range(0, 1), SerializeField] private int _function;
// 一个 delegate 数组用于保存我们接下来使用的两个函数
private Function[] _functions;
...
// Use this for initialization
private void Start()
{
// 初始化 _functions
_functions = new Function[] {SineFunction, MultiSineFunction};
...
}
private void Update()
{
_startX = -1f;
for (int i = 0; i < _resolution; i++)
{
var x = _startX + i * _step;
// 此处修改调用方法
var pos = new Vector3(x, _functions[_function](x, Time.time), 0);
var point = _points[i];
point.transform.localPosition = pos;
}
}
private float SineFunction(float x, float t)
{
return Mathf.Sin(Mathf.PI * (x + t));
}
private float MultiSineFunction(float x, float t)
{
float y = Mathf.Sin(Mathf.PI * (x + t));
y += Mathf.Sin(2f * Mathf.PI * (x + 2f * t)) / 2f;
y *= 2f / 3f;
return y;
}
}
于是我们实现了如下的效果~
不过作者在原文中还添加了 Enum 然后可以不用滑杆而是改用一个下拉菜单来改变要显示的函数图像。最终效果没什么不同就不再赘述了感兴趣的同学可以自行找到『原文链接』查看更详细的步骤~
PART 3 画出水滴的波纹
那么接下来开始要真正的绘制一个3D曲面了~那么首先是创建更多的小方块~我们在初始化的地方改成一个二维的 List 来保存所有的小方块
private void Start()
{
...
for (int i = 0; i < _resolution; i++)
{
_points.Add(new List<Transform>());
for (int j = 0; j < _resolution; j++)
{
var point = Instantiate(_cube, transform);
_points[i].Add(point.transform);
point.transform.localScale = scale;
point.SetActive(true);
}
}
}
在后续的遍历也对该二维数组进行遍历。
private void Update()
{
for (int i = 0; i < _points.Count; i++)
{
for (int j = 0; j < _points[i].Count; j++)
{
var posX = i * _step - 1;
var posZ = j * _step - 1;
var pos = new Vector3(posX, _functions[(int) _function](posX, posZ, Time.time), posZ);
var point = _points[i][j];
point.localPosition = pos;
}
}
}
最后再稍微修改下两个函数的参数就完成了从 2D 到 3D 的跳跃~如图所示
不过我们并不应该满足于此,感觉这样其实并没有充分利用 Z 轴啊,完全就是复制了很多条曲线排在一起。所以我们新建两个这样的函数。
private float Sine2DFunction(float x, float z, float t)
{
float y = Mathf.Sin(Mathf.PI * (x + t));
y += Mathf.Sin(Mathf.PI * (z + t));
y *= 0.5f;
return y;
}
private float MultiSine2DFunction(float x, float z, float t)
{
float y = 4f * Mathf.Sin(Mathf.PI * (x + z + t * 0.5f));
y += Mathf.Sin(Mathf.PI * (x + t));
y += Mathf.Sin(2f * Mathf.PI * (z + 2f * t)) * 0.5f;
y *= 1f / 5.5f;
return y;
}
那么 Sine2DFunction
可以很明显的看出是两个完全一样的正弦波分别沿 x 轴和 Z 轴传播并且直接叠加,那么第二个。。。反正很复杂语言解释不清楚大概就是 3 个波叠加起来的,大家可以一行一行注释掉看看效果就知道了~
那么如何画出一个波纹呢,首先波纹是由原点也就是(0, 0)
点开始均匀扩散的,那么可能是一个从原点向周围扩散的正弦波。那么直觉上来说这个函数可能长这样。。
private float Ripple (float x, float z, float t)
{
float d = Mathf.Sqrt(x * x + z * z);
float y = Mathf.Sin(Mathf.PI * (d - t));
return y;
}
运行下会发现完全不像,主要是因为水波在扩散的过程中是要衰减的,正弦波完全不会,因此我们需要加上衰减的控制。既然是衰减的话显然距离越大衰减的越多喽所以我们让 y 除以 1 + 2 * Mathf.PI * d
试一试,之所以加1是为了防止在距离原点过于近的时候结果趋近于无穷大。所以现在代码变成了这样~
private float Ripple(float x, float z, float t)
{
float d = Mathf.Sqrt(x * x + z * z);
float y = Mathf.Sin(Mathf.PI * (d - t));
y = y / (1 + 2 * Mathf.PI * d);
return y;
}
跑起来看一下会发现。。。emmmm
所以我们再加上一些参数比如_velocity
传播速度,frequency
水波频率,_amplitude
振幅,_attenuation
衰减。代码如下。(这些参数并不是数值越大就直观意义上越大,虽然这样不太好但是懒得整理了。。。大家大概意思理解就好)
private float Ripple(float x, float z, float t)
{
float d = Mathf.Sqrt(x * x + z * z);
float y = Mathf.Sin(_frequency * Mathf.PI * (d - t / _velocity));
y *= 1 / (_amplitude + _attenuation * 2 * Mathf.PI * d);
return y;
}
然后将这些参数调整到合适的值,就完成一个完美的水波了~如图所示
PART 4 画出三维图形
显然我们不能满足于此,传入 x 和 z 来计算出唯一的 y 导致了无法有两个点拥有相同的 x 和 z,这极大的限制了我们的发挥~比如说画出一个球体之类的。所以我们接下来的目标是画出真正的三维图形~
在开始之前,我们首先要放弃传入 x 和 z 来计算 y 的设想,所以应该把所有的函数的返回值改成 Vector3,并且为了区分我们将函数的参数变成 u,v,t。
public delegate Vector3 Function(float u, float v, float t);
public enum GraphFunctionName {
Sine,
MultiSine,
Sine2D,
MultiSine2D,
Ripple,
}
public class Graph3DController : MonoBehaviour
{
[Range(10, 100), SerializeField] private int _resolution;
[SerializeField] private GameObject _cube;
[SerializeField] public GraphFunctionName _function;
[SerializeField] private float _amplitude = 3;
[SerializeField] private float _frequency = 4;
[SerializeField] private float _velocity = 2;
[SerializeField] private float _attenuation = 6;
private List<List<Transform>> _points;
private float _step;
private Function[] _functions;
// Use this for initialization
private void Start()
{
_functions = new Function[] {SineFunction, MultiSineFunction, Sine2DFunction, MultiSine2DFunction, Ripple};
_cube.SetActive(false);
_points = new List<List<Transform>>();
_step = 2f / _resolution;
var scale = Vector3.one * _step;
for (int i = 0; i < _resolution; i++)
{
_points.Add(new List<Transform>());
for (int j = 0; j < _resolution; j++)
{
var point = Instantiate(_cube, transform);
_points[i].Add(point.transform);
point.transform.localScale = scale;
point.SetActive(true);
}
}
}
private void Update()
{
for (int i = 0; i < _points.Count; i++)
{
for (int j = 0; j < _points[i].Count; j++)
{
var u = i * _step - 1;
var v = j * _step - 1;
var point = _points[i][j];
point.localPosition = _functions[(int) _function](u, v, Time.time);
}
}
}
private Vector3 SineFunction(float u, float v, float t)
{
var x = u;
var y = Mathf.Sin(Mathf.PI * (u + t));
var z = v;
return new Vector3(x, y, z);
}
private Vector3 MultiSineFunction(float u, float v, float t)
{
var x = u;
float y = Mathf.Sin(Mathf.PI * (u + t));
y += Mathf.Sin(2f * Mathf.PI * (u + 2f * t)) / 2f;
y *= 2f / 3f;
var z = v;
return new Vector3(x, y, z);
}
private Vector3 Sine2DFunction(float u, float v, float t)
{
var x = u;
float y = Mathf.Sin(Mathf.PI * (u + t));
y += Mathf.Sin(Mathf.PI * (v + t));
y *= 0.5f;
var z = v;
return new Vector3(x, y, z);
}
private Vector3 MultiSine2DFunction(float u, float v, float t)
{
var x = u;
float y = 4f * Mathf.Sin(Mathf.PI * (u + v + t * 0.5f));
y += Mathf.Sin(Mathf.PI * (u + t));
y += Mathf.Sin(2f * Mathf.PI * (v + 2f * t)) * 0.5f;
y *= 1f / 5.5f;
var z = v;
return new Vector3(x, y, z);
}
private Vector3 Ripple(float u, float v, float t)
{
var x = u;
float d = Mathf.Sqrt(u * u + v * v);
float y = Mathf.Sin(_frequency * Mathf.PI * (d - t / _velocity));
y *= 1 / (_amplitude + _attenuation * 2 * Mathf.PI * d);
var z = v;
return new Vector3(x, y, z);
}
}
圆柱体
那么如何组成一个圆柱体呢,首先我们知道圆柱体可以认为是由许多个圆环组成的,那么如何构成一个圆环呢?我们知道 u 的取值范围是[-1, 1],将 u * PI 即可获得 [-PI, PI] 即刚好一个圆周的弧度,对应的坐标即是(x = sin(PI * u), z = cos(PI * u))
,按照以上思路我们完成以下代码。然后每一个点的纵座标 y 就直接取 v 的值即可形成「每个水平的圆周上有100个点,共100个圆纵向排列组成的圆柱体」了好吧感觉表述的不是特别清楚写出来跑跑看就知道了。。。
private Vector3 Cylinder(float u, float v, float t)
{
var x = Mathf.Sin(Mathf.PI * u);
var y = v;
var z = Mathf.Cos(Mathf.PI * u);
return new Vector3(x, y, z);
}
运行一下发现果然是一个圆柱体,如果想要控制圆柱体的半径和高直接在 x 和 z 乘以 R,y 乘以 H 即可,如下图所示。代码就不贴了大家都会自己乘~
那么如何让这个圆柱体动起来呢~比如说随便对 R 做一些手脚像下面这样
private Vector3 InterestingCylinder(float u, float v, float t)
{
var r = _radius * (0.8f + Mathf.Sin(Mathf.PI * (6f * u + 2f * v + t)) * 0.2f);
var x = r * Mathf.Sin(Mathf.PI * u);
var y = _height * v;
var z = r * Mathf.Cos(Mathf.PI * u);
return new Vector3(x, y, z);
}
尝试改变 u 和 v 的系数可以看到很多有趣的现象哦~懒得自己写的可以打开我的「Github Repo」直接运行时修改 FactorU 和 FactorV 的值查看结果~最终我们可以达到类似这样的效果
球体
我们在圆柱体的基础上稍加修改就可以获得一个球体,首先,球体跟圆柱体一样也可以认为是很多半径不同的圆环组成的,那么圆环的半径呈现怎样的变化呢,我们想象球体沿经线切开后,可以观察到一圈纬线的半径和纬线的纵座标分别对应Cos(PI / 2 * v)
和Sin(PI / 2 * v)
,按照这个思路我们尝试写出如下代码。
private Vector3 Sphere(float u, float v, float t)
{
var r = _radius * Mathf.Cos(Mathf.PI / 2 * v);
var x = r * Mathf.Sin(Mathf.PI * u);
var y = _radius * Mathf.Sin(Mathf.PI / 2 * v);
var z = r * Mathf.Cos(Mathf.PI * u);
return new Vector3(x, y, z);
}
运行一下发现完全没有问题~如图所示。。。
所以想要让球体动起来我们可以使用同样地思路对 r 的计算进行一点点魔改,比如说这样的一个参数factor
:
private Vector3 InterestingSphere(float u, float v, float t)
{
var factor = 0.8f + Mathf.Sin(Mathf.PI * (_factorU * u + t)) * 0.1f;
factor += Mathf.Sin(Mathf.PI * (_factorV * v + t)) * 0.1f;
var r = factor * _radius * Mathf.Cos(Mathf.PI / 2 * v);
...
}
调一些奇怪的参数。。。然后就出现了一坨嚅动的,。。球体。。。
圆环体
那么想象下一个圆环体和球体到底有什么区别呢,针对每左半条或者右半条经线圈,如果直接变成一个环,那么球体不就变成圆环了么。。。那么怎么变成圆环呢,我们之前提到
一圈纬线的半径和纬线的纵座标分别对应
Cos(PI / 2 * v)
和`Sin(PI / 2 * v)
所以我们把半个周期的 cos 和 sin 变成完整周期就可以了,不要除以 2 就好。。于是我们尝试着写下如下代码
private Vector3 Torus(float u, float v, float t)
{
var r = _radius * Mathf.Cos(Mathf.PI * v);
var x = r * Mathf.Sin(Mathf.PI * u);
var y = _radius * Mathf.Sin(Mathf.PI * v);
var z = r * Mathf.Cos(Mathf.PI * u);
return new Vector3(x, y, z);
}
运行一下发现还是球体啊。。这是为什么呢,仔细观察发现似乎小方块比以前稀疏了,是因为半条经线被扩展到整个周期以后变成了一整圈经线,所以和对面的那半条完全重叠了。。所以怎么解决这个问题呢?就是扩大纬线圈让相对的两个半条经线不会相互重叠甚至完全分离就可以了。所以这样修改下试试
private Vector3 Torus(float u, float v, float t)
{
var r = _radius * Mathf.Cos(Mathf.PI * v) + _radius2;
...
}
这里之所以是加一个_radius2
在最外面是为了达到「无论 v 如何变化都可以是的半径无条件增加 _radius2」的效果。。。运行下会发现嗯果然没问题了。。
所以最后也顺便让它动起来吧。。。
PART 5 总结
好吧这篇真的好长,而且写的好累并且在公式功能坏掉的情况下又很难讲清楚~大家把「Github Repo」下载下来自己运行稍微修改下就很容易理解了~总之我们把简单的图像扩展到了三维的图形的过程还是很有趣的~虽然不知道暂时有什么用处不过对于培养数学思维也还是挺有帮助的~好吧希望下一篇早日更新~就酱。。。
原文链接:https://snatix.com/2018/06/20/021-mathematical-surfaces/
本文由 sNatic 发布于『大喵的新窝』 转载请保留本申明
Catlike学习笔记(1.3)-使用Unity画更复杂的3D函数图像的更多相关文章
- Catlike学习笔记(1.2)-使用Unity画函数图像
『Catlike系列教程』第二篇来了~今天周六,早上(上午11点)醒来去超市买了一周的零食回来以后就玩了一整天游戏非常有负罪感.现在晚上九点天还亮着感觉像下午7点左右的样子好像还不是很晚...所以就写 ...
- Catlike学习笔记(1.4)-使用Unity构建分形
又两个星期没写文章了,主要是沉迷 Screeps 这个游戏,真的是太好玩了导致我这两个礼拜 Github 小绿点几乎天天刷.其实想开一个新坑大概把自己写 AI 的心路历程记录下,不过觉得因为要消耗太多 ...
- Catlike学习笔记(1.1)-使用Unity实现一个钟表
最近发现『Catlike系列教程』觉得内容真的很赞,感觉有很多地方涉及到了我的知识盲点,如果真的可以照着做下来一遍的话应该收获颇丰.因为教程很长所以逐字翻译不太可能了(主要是翻译的太差).基本上就是把 ...
- C++ 学习笔记 (六) 继承- 子类与父类有同名函数,变量
学习了类的继承,今天说一下当父类与子类中有同名函数和变量时那么程序将怎么执行.首先明确当基类和子类有同名函数或者变量时,子类依然从父类继承. 举例说明: 例程说明: 父类和子类有同名的成员 data: ...
- Directx11学习笔记【十二】 画一个旋转的彩色立方体
上一次我们学习了如何画一个2D三角形,现在让我们进一步学习如何画一个旋转的彩色立方体吧. 具体流程同画三角形类似,因此不再给出完整代码了,不同的部分会再说明. 由于我们要画彩色的立方体,所以顶点结构体 ...
- pygame学习笔记(2)——从画点到动画
转载请注明:@小五义 http://www.cnblogs.com/xiaowuyi 1.单个像素(画点)利用pygame画点主要有三种方法:方法一:画长宽为1个像素的正方形 #@小五义 http:/ ...
- Unity3D学习笔记(四)Unity的网络基础(C#)
一 网络下载可以使用WWW类下载资源用法:以下载图片为例WWW date = new WWW("<url>");yield return date;texture = ...
- Unity3D学习笔记(三)Unity的C#基础
在C#脚本中,必须显式的继承MonoBehaviour类需要注意的是,在创建C#脚本时,脚本名应尽量符合C#命名规则,以字母或下划线开头,因为类名的默认跟随脚本名.C#声明变量的方式和C++和Java ...
- Unity3D学习笔记(二)Unity的JavaScript基础
Update()每帧调用一次LateUpdate()在Update()后执行Awake()系统执行的第一个方法Start()在Awake()之后,Update()之前FixedUpdate()固定更新 ...
随机推荐
- Django中的DateTimeField格式
转自:http://www.nanerbang.com/article/5488/ 创建django的model时,有DateTimeField.DateField和TimeField三种类型可以用来 ...
- Linux 快速查看系统配置-熟悉新环境的配置
问题背景: 当我们使用新的环境的时候,需要很快得熟悉自己环境的配置,这时候我们如果知道一些命令就极为方便了.这样你就能对自己的环境较为熟悉,进行工作的时候也能随心所欲了. 如果你使用workstati ...
- Online, Cheap -- and Elite
Online, Cheap -- and Elite Analysis of Georgia Tech’s MOOC-inspired online master's in computer scie ...
- 看代码网备份|利用WebClient|eKing.CmdDownLoadDbBakOper|实现定时拷贝数据库备份文件到文件服务器
摘要: 1.有两台服务器 (1)看代码网(记为A):内网IP:10.186.73.30 (2)文件服务器(记为B):内网IP:10.135.87.157 2.在A架设一个网站,端口8088(防火强设置 ...
- linux安装mydumper软件包以及报错解决
今天使用mydumper命令从AWS上的RDS集群MYSQL数据库导出数据,发现Tidb官方提供的工具不太适合,所以就自己编译了一个来尝试一下,居然成功了. 首先我的系统是Centos7,并且已经安装 ...
- javascript,object,IDispatchEx笔记
//js: var testObj=new Object; //com内部: testObj=Object::InvokeEx(wFlags==DISPATCH_CONSTRUCT); //注: // ...
- 使用C#获取Windows Phone手机的各种数据(转)
转自:http://www.sum16.com/desinger/use-c-sharp-get-windows-phone-information.html 使用C#获取Windows Phone手 ...
- Asp.net中DataTable的排序功能
DataTable里的数据,如果是从数据库中取得的数据,我们可以用order by排序,而从excel表格取得的数据,就需要自己进行操作了. 例如,Dt_Data2是读取Excel表格取到的数据 Da ...
- Hash问题----Hash强碰撞
包含内容:hellowword,byeworld文件md5,pdf1,2的sha1值. 等待笔记...
- Ecstore Nginx Rewrite(去掉链接中的index.php) ECSTORE 伪静态
一.修改 nginx.conf文件,添加如下代码: if ($request_uri ~ (.+?\.php)(|/.+)$ ){ break; } if (!-e $request_filename ...