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()固定更新 ...
随机推荐
- CentOS7 中安装 MySQL
0. 说明 参考 centos7.2安装MySQL CentOS 7 下 Yum 安装 MySQL 5.7 两种方式安装 MySQL 安装 MySQL(yum) & 安装 MySQL(yum) ...
- Mysql学习---面试基础知识点总结
drop.truncate. delete区别 TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同:二者均删除表中的全部行.但 TRUNCATE TABLE 比 ...
- C#中抽象类(abstract)和接口(interface)的实现
抽象类 抽象方法是没有代码实现的方法,使用abstract关键字修饰: 抽象类是包含0到多个抽象方法的类,其不能实例化.含有抽象方法的类必须是抽象类,抽象类中也可以包含非抽象方法: 重写抽象类的方法用 ...
- 基于Map的简易记忆化缓存
背景 在应用程序中,时常会碰到需要维护一个map,从中读取一些数据避免重复计算,如果还没有值则计算一下塞到map里的的小需求(没错,其实就是简易的缓存或者说实现记忆化).在公司项目里看到过有些代码中写 ...
- Tensorflow Object Detection API 安装
git:https://github.com/tensorflow/models/tree/master/object_detection 中文文档:http://wiki.jikexueyuan.c ...
- ocr jdk
公司有个需求,遍历所有图片,筛选出含有敏感字的图片.这里就需要ocr技术,找了几天,发现了几个不错的ocr jdk. http://cn.ocrsdk.com/ 俄罗斯公司,贵有贵的道理 http:/ ...
- laravel记录笔记Laravel 连接数据库、操作数据库的三种方式
laravel中提供DB facade(原始查找).查询构造器.Eloquent ORM三种操作数据库方式 1.连接数据库 .env 数据库配置 DB_HOST=localhost dbhost DB ...
- leetcode15—3Sum
Given an array nums of n integers, are there elements a, b, c in nums such that a + b + c = 0? Find ...
- Centos7 安装Nodejs
使用EPEL安装 EPEL(Extra Packages for Enterprise Linux)企业版Linux的额外软件包,是Fedora小组维护的一个软件仓库项目,为RHEL/CentOS提供 ...
- Python2.7-matplotlib
matplotlib的pyplot子库提供了和matlab类似的绘图API,方便用户快速绘制2D图表 一般用以下形式导入:import matplotlib.pyplot as plt 一般用法:1. ...