文章目录
前言
学习catlike coding教程的base篇第二章个人笔记。
一、函数库的制作
1.已有脚本展示
Graph类,其实就是一个创建出游戏对象的脚本,把这些游戏对象缓存下来,并且使用该脚本控制游戏物体空间坐标和缩放。
使用步骤就是在游戏创建中创建一个新的空物体,并且把Graph脚本挂载到空物体上面。
代码演示:
public class Graph : MonoBehaviour
{
[SerializeField] public Transform pointPrefabs;
[SerializeField] public int createPointNum = 100; //创建点的数量
[SerializeField] public float pointDist = 0.1f; //点和点之间的距离
[SerializeField] public FunctionLibrary.FuncName f = FunctionLibrary.FuncName.Wave;
private Transform[] _transforms;
private Transform _trans;
void Start()
{
_trans = transform;
//统一修改预制体大小
pointPrefabs.localScale = new Vector3(0.05f,0.05f,0.05f);
_transforms = new Transform[createPointNum];
for (int i = 0;i<createPointNum;i++)
{
Transform newTrans = Instantiate(pointPrefabs);
newTrans.gameObject.name = $"point{i}";
newTrans.SetParent(_trans);
_transforms[i] = newTrans;
}
}
//让图像动起来
void Update()
{
Tick();
}
[HideInInspector] private Vector3 _cache = Vector3.zero;
void Tick()
{
float time = Time.time;
for (int i = 0;i < _transforms.Length;i++)
{
float x = i * pointDist;
_cache.x = x;
_cache.y = Mathf.Sin(x*2 + time);
_transforms[i].localPosition = _cache;
}
}
}
这里的效果就是简单的绘制一个挥动的Sin振幅图像,下面是gif演示:
由于子节点使用了局部坐标,所以可以自由的修改父节点的空间坐标,旋转,缩放。
2.使用枚举和委托制作函数库
上面的这段代码
void Tick()
{
float time = Time.time;
for (int i = 0;i < _transforms.Length;i++)
{
float x = i * pointDist;
_cache.x = x;
_cache.y = Mathf.Sin(x*2 + time);
_transforms[i].localPosition = _cache;
}
}
这一句 _cache.y = Mathf.Sin(x2 + time);。这是一句根据物体的x坐标来获得物体的y坐标的代码。我们可以稍微封装一下这个函数,申明一个新的类型FunctionLibrary,其中把Mathf.Sin(x2+time)加进去,这里使用静态修饰而无需创建FunctionLibrary对象,就有以下代码:
public static class FunctionLibrary
{
public static float DrawSin(float x,float t)
{
return Mathf.Sin(x * 2 + t);
}
}
并且这里改成使用一个委托去代替Math.Sin(x*2 + time),而这个委托只需要坐标的x属性和时间变化值这两个变量。
FunctionLibarary改成
public static class FunctionLibrary
{
public delegate float Function(float x,float t);
public static float DrawSin(float x,float t)
{
return Mathf.Sin(x * 2 + t);
}
}
这样Graph的Tick函数就可以改成:
void Tick()
{
FunctionLibrary.Function f = FunctionLibrary.DrawSin;
float time = Time.time;
for (int i = 0;i < _transforms.Length;i++)
{
float x = i * pointDist;
_cache.x = x;
_cache.y = f(x,time);
_transforms[i].localPosition = _cache;
}
}
再写入一个委托的数组和一个枚举的声明,最好就改成这样:
public static class FunctionLibrary
{
public delegate float Function(float x,float t);
public static Function[] functions = {
DrawSin};
public enum FuncName {
DrawSin }
public static float DrawSin(float x,float t)
{
return Mathf.Sin(x * 2 + t);
}
public static Function GetFunc(int index)
{
return functions[index];
}
}
这里主要是使用funtions数组,FuncName枚举,GetFunction函数来索引到对应的函数。再对FunctionLibrary引入多个函数就有:
public static class FunctionLibrary
{
public delegate float Function(float x,float t);
public static Function[] functions = {
DrawSin,Wave,MultiWave,Ripple,RippleAnim};
public enum FuncName {
DrawSin,Wave,MultiWave,Ripple,RippleAnim }
public static float DrawSin(float x,float t)
{
return Mathf.Sin(x * 2 + t);
}
public static Function GetFunc(int index)
{
return functions[index];
}
public static float Wave(float x, float t)
{
float y = Sin(PI * (x + t));
y += 0.5f * Sin(2f * PI * (x + t));
return y / 1.5f;
}
public static float MultiWave(float x, float t)
{
float y = Sin(PI * (x + 0.5f * t));
y += 0.5f * Sin(2f * PI * (x + t));
return y / 1.5f;
}
public static float Ripple(float x, float t)
{
float d = Abs(x);
return Sin(PI * 4.0f * d);
}
public static float RippleAnim(float x, float t)
{
float d = Abs(x);
float y = Sin(PI * (4.0f*d - t));
return y / (1.0f + 10.0f * d);
}
}
最后Graph应该做出的修改:
[SerializeField] public FunctionLibrary.FuncName funcIndex = FunctionLibrary.FuncName.DrawSin;
[HideInInspector] private Vector3 _cache = Vector3.zero;
void Tick()
{
FunctionLibrary.Function f = FunctionLibrary.GetFunc((int)funcIndex);
float time = Time.time;
for (int i = 0;i < _transforms.Length;i++)
{
float x = i * pointDist;
_cache.x = x;
_cache.y = f(x,time);
_transforms[i].localPosition = _cache;
}
}
这样就简单的完成了一个函数库,并且可以在Inspector面板上自由选择函数。
3.这里稍微放一下,下面会使用到的一个简单的shader
shader "Custom/PointShader"
{
SubShader{
Tags {
"RenderType" = "Opaque"}
LOD 200
Pass{
CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
struct adp_data
{
float4 vert:POSITION;
};
struct v2f
{
float4 vert:SV_POSITION;
fixed4 color:TEXCOORD0;
};
v2f vert(adp_data i)
{
v2f o;
o.vert = UnityObjectToClipPos(i.vert);
o.color = mul(unity_ObjectToWorld,i.vert);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed4 outColor = i.color;
outColor = saturate(outColor + (0.5,0.5,0.5));
return outColor;
}
ENDCG
}
}
FallBack "Diffuse"
}
二、曲面图像绘制
1.简单实现一个带有对角波动的图像
上面的图像是仅仅使用了x轴和y轴。下面把z轴也用上来。
首先定义一个值,用于每次计算完多少个数量的代表点的物体时,把用于计算物体x坐标的变量进行一次重置,并且迭代一次计算z变量的数值。而这里我每次计算x和z开始的时候都是选择使用对称的坐标,所以就有一下代码更新出来:
public class Graph : MonoBehaviour
{
[SerializeField] public Transform pointPrefabs;
[SerializeField] public int createPointNum = 100; //创建点的数量
[SerializeField] public float pointDist = 0.1f; //点和点之间x轴的距离
[SerializeField] public float zPointDist = 0.05f; //点和点之间z轴的距离
[SerializeField] public int max = 100; //新增变量
[SerializeField] public float scale = 1.0f; //主要控制代表点的缩放
private Transform[] _transforms;
private Transform _trans;
void Start()
{
_trans = transform;
//统一修改预制体大小
pointPrefabs.localScale = new Vector3(scale,scale,scale);
_transforms = new Transform[createPointNum];
for (int i = 0;i<createPointNum;i++)
{
Transform newTrans = Instantiate(pointPrefabs);
newTrans.gameObject.name = $"point{i}";
newTrans.SetParent(_trans);
_transforms[i] = newTrans;
}
}
//让图像动起来
void Update()
{
Tick();
}
[SerializeField] public FunctionLibrary.FuncName funcIndex = FunctionLibrary.FuncName.DrawSin;
[HideInInspector] private Vector3 _cache = Vector3.zero;
void Tick()
{
FunctionLibrary.Function f = FunctionLibrary.GetFunc((int)funcIndex);
float time = Time.time;
int index = GetStartX();
int zIndex = - _transforms.Length / max / 4; //计算对称的z开始的值
for (int i = 0;i < _transforms.Length;i++)
{
float x = index * pointDist;
float z = zIndex * zPointDist;
_cache.x = x;
_cache.y = f(x,time);
_cache.z = z;
_transforms[i].localPosition = _cache;
index++;
if (index >= max){
index = GetStartX();
zIndex++;
}
}
}
int GetStartX()
{
//计算对称x开始的值
return -_transforms.Length / 2 <= -max ? -max : -_transforms.Length / 2;
}
}
修改好上面的代码之后就有一下的效果:
而这里,我们知道根据上面那张gif图,这个图像里面的坐标轴箭头为z轴,指向为正的方向,所以该图像由右边往左边z逐渐加大,当点足够多的时候,我们可以认为z的值也是线性的在修改。当我们往普通的振幅函数DrawSin里面引用z变量时就会有以下效果:
可想象出来吧,z值带来的x值的变化刚刚好可以引起这种对角线的变化。修改代码如下:
public static float DrawSin(float x,float z,float t)
{
return Mathf.Sin( PI * (x + z + t))/2;
}
这样就简单实现了一个带有对角线波动的3D图像。
2.实现一个混合对角波动的图像
当然也可以给z的方向也做一个波动,就有以下,修改以下MultiWave函数:
public static float MultiWave(float x,float z, float t)
{
float y = Sin(PI * (x + 0.5f * t));
y += 0.5f * Sin(2f * PI * (z + t));
y += Sin(PI * (x + z + 0.25f * t));
return y*(1f/2.5f);
}
上面的代码相当于是给z的方向做了两个振幅:
振幅1, y += 0.5f * Sin(2f * PI * (z + t));。里面的2f乘以PI是加快振幅的频率,外面乘以0.5是把这个振幅的最小峰值和最大赋值降低一下。
振幅2,y += Sin(PI * (x + z + 0.25f * t));。这里是添加对角线振幅,切把时间调慢至0.25倍。
最后的 y / 2.5f,主要是因为上面的振幅加起来的赋值会变成原来的2.5倍。为了控制在1倍到-1倍。
最终效果如下:
3.实现一个按圆波动的图像
上面实现了一个比较复杂的混合波动的图像,当然我们也可以带入圆的计算公式。按照xx + zz来添加波动,并且越接近x = 0,z = 0的情况下,y的返回值就越大。这样可以实现一下水滴的效果,代码如下:
public static float Ripple(float x,float z, float t)
{
float d = Sqrt(x*x + z*z);
float y = Sin(PI * (4f * d - t));
return y / (1 + 10 * d);
}
下面为最终效果:
三.3D封闭图像的实现
上面的基本上都是实现了曲面所以表达式f(x,z) = y基本山都可以实现出来,但是我们知道有一个局限性,那就是在3D空间里面拥有相同的y坐标的点是不能同时引用相同的x,z坐标的,所以得把表达式修改成一下:
f(u,v) = (x,y,z)。有一个uv的坐标系去得到一个坐标点。所以函数库得稍微修改一下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEngine.Mathf;
public static class FunctionLibrary
{
public delegate Vector3 Function(float x,float z,float t);
public static Function[] functions = {
DrawSin,Wave,MultiWave,Ripple};
public enum FuncName {
DrawSin,Wave,MultiWave,Ripple}
public static Vector3 DrawSin(float u,float v,float t)
{
Vector3 p;
p.x = u;
p.z = v;
p.y = Mathf.Sin( PI * ( u + v + t))/2;
return p;
}
public static Function GetFunc(int index)
{
return functions[index];
}
public static Vector3 Wave(float u,float v, float t)
{
Vector3 p;
p.x = u;
p.z = v;
float y = Sin(PI * (u + t));
y += 0.5f * Sin(2f * PI * (v + t));
p.y = y / 1.5f;
return p;
}
public static Vector3 MultiWave(float u,float v, float t)
{
Vector3 p;
p.x = u;
p.z = v;
float y = Sin(PI * (u + 0.5f * t));
y += 0.5f * Sin(2f * PI * (v + t));
y += Sin(PI * (u + v + 0.25f * t));
p.y = y * (1f / 2.5f);
return p;
}
public static Vector3 Ripple(float u,float v, float t)
{
Vector3 p;
p.x = u;
p.z = v;
float d = Sqrt(u*u + v*v);
float y = Sin(PI * (4f * d - t));
p.y = y / (1 + 10 * d);
return p;
}
}
上面的函数的返回值都修改成了Vector3。
1.创建3D复杂对象
我们可以使用这段代码来创建一个球出来
public static Vector3 Sphere(float u,float v, float t)
{
Vector3 p;
float r = Cos(0.5f * PI * v);
p.x = r * Sin(u * PI);
p.y = Sin(v * 0.5f * PI);
p.z = r * Cos(u * PI);
return p;
}
这段代码实际上就是靠p.x = Sin(u * PI);和p.z = Cos(u * PI);来给x - z平面创建一个圆。后面根据 v的值来给这个圆创建不同的半径所以就有了 float r = cos(0.5f * PI * v);这句代码。而y是根据Sin(v * 0.5f * PI);决定的。
其实这个只是看起来像球的胶囊。
最终效果如下:
这里使用创建更为复杂的公式来创建一个图形,代码如下:
public static Vector3 Touch(float u,float v, float t)
{
float r1 = 0.7f + 0.1f * Sin(PI * (6f * u + 0.5f * t));
float r2 = 0.15f + 0.05f * Sin(PI * (8f * u + 4f * v + 2f * t));
float s = r1 + r2 * Cos(PI * v);
Vector3 p;
p.x = s * Sin(PI * u);
p.y = r2 * Sin(PI * v);
p.z = s * Cos(PI * u);
return p;
}
最终效果为: