|
|
9 місяців тому | |
|---|---|---|
| .. | ||
| Image | 9 місяців тому | |
| src | 1 рік тому | |
| README.md | 9 місяців тому | |
噪音本质上是一个函数,输入一个参数,一般来说返回一个 0 ~ 1 之间的值
噪音函数要求随机性和确定性,随机性是值随机,确定是输入相同的参数会得到相同的结果
于是随便定义一个函数
float random(float x) {
return fract(sin(x * 100) * 5432.1234);
}
fract函数表示取小数点x * 100和5432.1234是随便写的数值,主要是增加频率,来增加变化率
得到上图所示的一个图像,这个图像满足一定的随机性和确定性,但是有一个问题就是点与点之间的过度不够平滑
用噪声图生成河流山脉会要求过度平滑,所以噪声函数在输入的点之间也会要求平滑过度
| 平滑 | 不平滑 |
|---|---|
![]() |
![]() |
平滑函数 $y = xx(3 - 2*x)$
通过上面的平滑函数可以将两点之间数值的变化通过插值的方式平滑过度,虽然平滑函数在小于0和大于1之后函数取值会超过指定范围,但是在 0~1 区间是非常平滑的
并且在值为0和值为1的过度点也是平滑过度的,所以能够平滑的将多个区间连接起来
float random(float x) {
return fract(sin(x * 1000.0) * 5432.123);
}
float noise(float seed) {
float i = floor(seed);
float f = fract(seed);
float a = random(i);
float b = random(i + 1.0);
float u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u);
}
mix线性插值函数
通过 random(i) 和 random(i + 1) 确定两点的 y 值
通过 fract(seed) 获取目标点 seed 在两点之间的比率,通过平滑函数计算差值
如果通过 mix(a, b, f) 计算得到的是曲线是一条直线
如果通过 mix(a, b, u) 计算得到的是一个光滑曲线
同理,将类似概念引入二维
float random2(vec2 x) {
return fract(sin(dot(x.xy, vec2(12.9888, 78.233))) * 5432.123);
}
vec2(12.9888, 78.233)是随便写的数值,主要是为了增加频率
这个时候可以得到一个过度非常生硬的噪声图
于是乎,也要使用平滑插值函数来
float noise2(vec2 seed) {
vec2 i = floor(seed);
vec2 f = fract(seed);
float bl = random2(i);
float br = random2(i + vec2(1.0, 0.0));
float tl = random2(i + vec2(0.0, 1.0));
float tr = random2(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
float b = mix(bl, br, u.x);
float t = mix(tl, tr, u.x);
return mix(b, t, u.y);
}
这里使用双线性插值来计算点真正的颜色
如上图所示
TL 表示 Top Left 表示左上角TR 表示 Top Right 表示右上角BL 表示 Bottom Left 表示左下角BR 表示 Bottom Right 表示右下角所以进行插值计算的时候 mix(bl, br, u.x) 和 mix(tl, tr, u.x) 都是对 uv.x 进行插值插值计算
通过一系列的计算,可以得到过度非常平滑的噪声图
上图代码中的
noiseScale是为了提高噪音的频率,自行修改可知作用
还可以通过叠加多个噪声图的方式来得到新的噪音图
float addNoise2(vec2 seed) {
float result = noise2(seed * 8.0) * 1.0;
result += noise2(seed * 16.0) * 0.5;
result += noise2(seed * 32.0) * 0.25;
result += noise2(seed * 64.0) * 0.125;
result += noise2(seed * 128.0) * 0.125;
return result / 2.0;
}
由于叠加多个噪音图,并且所有噪音图的权重合为 2,所以最后 result 要除以 2
用于 addNoise2 中已经乘以了噪音频率,所以传入参数时不用像之前一样乘以一个频率
使用上面的代码,可以尝试编写一个律动的圆
大致思路如下
在 Shader 中要绘制一个圆形很简单,计算点到圆形的距离,距离在指定范围内是圆形的颜色,否则是背景的颜色
在圆形中根据点的坐标可以计算圆心到点的朝向,要做出如上图所示的不规则的圆,只要每个朝向都加减一个数值即可
这里的朝向其实就是圆心到点的向量
也就是说圆心到每个朝向都有一个随机值,绘制圆形的时候,距离在半径 + 随机值之内的点是圆形的颜色
对于一个在噪声图上的圆来说,圆周之上每个点的过度都是平滑的
所以将归一化的向量(归一化保证相同方向上点的向量相同)在噪声图上计算出一个值,那么这个方向圆的半径是半径+噪音值,由于噪音图中点的过度是平滑,所以圆的连续方向上的半径过度也是平滑的
在在噪声图上从 圆1 移动到 圆2 过程中(也就是 点A 移动到 点A'),点的过度也应该是平滑的,所以为了让圆律动起来,让圆心随着时间变化移动起来即可
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec4 bgColor = vec4(0.0, 0.0, 0.0, 1.0);
vec4 circleColor = vec4(1.0, 1.0, 1.0, 1.0);
const float radius = 0.5;
const float changeRateFactor = 0.1; // 噪音图上采样,数值越小越容易连续
const float speedFactor = 1.0; // 速度变化参数,采样中心点偏移速度
const float radiusBuffer = 0.3; // 噪声采样,值越大形变范围越大
vec2 uv = fragCoord/iResolution.xy; // 将屏幕坐标fragCoord转换为0到1的规范化坐标
vec2 suv = uv * 2.0 - 1.0; // 将uv坐标缩放并平移到-1到1的范围内,使得原点位于屏幕中心
suv.x *= iResolution.x / iResolution.y; // 调整x坐标,考虑屏幕的宽高比,确保圆形不会变形
vec2 dir = normalize(uv - vec2(0.0));
float noiseValue = noise2(uv + dir * changeRateFactor + vec2(iTime) * speedFactor);
noiseValue *= radiusBuffer;
fragColor = mix(bgColor, circleColor, step(length(suv), radius + noiseValue));
}
length计算当前片段的坐标点到原点的距离
简单实现原理
Actor 身上挂一个 PlanePlane 根据 Radius 做缩放,根据 Angle 做夹角限制, Shader 里携程扇形 FanFan 基础上,根据像素是否被障碍物遮挡,做剔除类似 ShadowMap,需要使用深度图
就是要绘制一个圆形出来,uv 两轴的取值范围是 0~1,所以 uv * 2 - 1 然后通过 length 计算点到圆形半径,小于 1 显示,否则不显示
根据向量点乘可以计算向量之间的夹角,指定角度之内的显示,之外的不显示
UE 封装了
VectorToRadialValue节点可以方便的获取UV转到圆的各个参数
然后一个做法是,给角色一个相机专门用来获取深度信息,根据像素的坐标和深度图的信息可以计算得到像素点是否应该被绘制
另一个做法是使用射线检测,将射线检测到的点通过顺序连线构成一个多边形,并绘制到一个 RenderTarget 中
使用上图所示的结构体和接口,可以绘制多个三角形到 Render Target 2D 中,注意每个点的颜色和透明度都要设置,并且透明度一定要不能忘记,否则点是透明的显示效果是错误的
在绘制之前需要使用 Clear Render Target 2D 来清除 RenderTarget 中的内容,防止上一帧的数据影响这一帧的内容
这里使用射线检测,存储点的信息
根据射线检测的 HitResult 可以获得射线检测的碰撞信息、起始点、终点、碰撞点,将碰撞点映射到 RenderTarget 贴图上
然后在材质中根据贴图采样和原本的扇形进行运算,得到重合区域,将其显示出来
如果每个 Actor 都挂载 Plane 并每帧做射线检测,可能会导致性能问题
对于每个 Actor 都挂载的情况,可以不每帧做射线检测,而是是同 Timer 的方式定时调用,齐次可以设置射线检测的角度间隔来减少射线检测的数量
又或者,只用一张很大的 RenderTarget 贴在地面,然后将所有 Actor 的射线检测之后的视线范围都投射到这张大的 RenderTarget 中
如此一来只用一张 RenderTarget 就可以一次性绘制
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
float Multiple = 8.0;
float PI = 3.1315926535;
vec2 uv = fragCoord/iResolution.xy;
// 将值域变化为 -1 ~ 1
float posX = (uv.x - 0.5) * 2.0;
float posY = (uv.y - 0.5) * 2.0;
// 修正: 调整 UV 坐标到中心对称且保持宽高比 上面代码无法保证宽高比 导致跟随图片尺寸拉伸
vec2 uv2 = (2.0 * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y);
posX = uv2.x;
posY = uv2.y;
// 得到角度 -PI ~ PI
float angle = atan(posY, posX);
float dis = length(vec2(posX, posY));
// 角度转换为 0 ~ PI 且上下对称
// angle = abs(angle / PI);
// angle = mod(angle + iTime, 1.0);
// fragColor = vec4(vec3(angle), 1.0);
// 角度转换为 0 ~ PI 并且中心对称
// float hit = mod(angle / PI+ iTime, 1.0); // 重点函数 使用 mod 而不是 abs
// vec3 col = mix(vec3(0.), vec3(1.), hit);
// fragColor = vec4(col, 1.0);
// * Multiple 是为了实现多段条纹
// 加上 1.0 / dis 是为了通过反函数来实现条纹扭曲的效果
// 最后乘以 dis 保证屏幕中间的暗 来实现黑洞效果
float hit = sin((angle / PI + iTime + 1.0 / dis) * Multiple * PI) * dis;
vec3 col = mix(vec3(0.), vec3(1.), hit);
fragColor = vec4(col, 1.0);
}
核心思路就是通过极坐标的角度来表示颜色变化
对面下面两种计算方法得到的颜色差异
float hit = mod(angle / PI+ iTime, 1.0);
vec3 col = mix(vec3(0.), vec3(1.), hit);
fragColor = vec4(col, 1.0);
angle = abs(angle / PI);
angle = mod(angle + iTime, 1.0);
fragColor = vec4(vec3(angle), 1.0);
多用一个 abs