shader噪声
shader中噪声图的生成
这边文章主要讲噪声图的生成原理,以及噪声图在shader中的应用。
什么是噪声图
简单直白地说人话,噪声图就是一张有着如同云雾一般混乱画面的图片。
噪声的生成原理有什么用?
一般情况下,我们需要用噪声图时,直接在网上找就行了。但是有时候,我们可能找不到需要的类型的图,或者是移植了shadertoy上的shader代码,由于shadertoy上很多都是在用实时计算,在拿来应用时,可能需要将保存成图片来优化计算量。
常规代码中的随机数生成
在常规的cpu上执行的代码,我们一般用这种形式的算法生成随机数:
随机数 = (随机数种子 + a ) 模除 b。
使用这种随机数生成算法,需要给定一个初始的随机数种子。之后,每次生成随机数,将使用上一个随机数作为这次随机的随机数种子。
公式很简单,主要是a和b的值需要选的好,这样生成的随机数才会比较像随机。
这个随机数计算方法放在噪声图生成中有个严重的问题。大家想想看是什么?
悬浮鼠标到此查看
一元方程式随机数
噪声图的生成是一个2维图像。但是为了方便理解算法,我们先在考虑在一维中研究算法。
1算法的输入值
对于每个像素点来说,各自特有不与其他像素点相同的数据,其中很自然就能想到的就是像素点的坐标。
2输出范围限定
接下来的这些图片来自于
https://thebookofshaders.com/10/?lan=ch
可以到这个网页上自己修改尝试各种写法。
生成随机数必然有一个范围,在数学计算中,y = f(x),y的值永远处于一定范围内,很容易就能想到三角函数。
我们先不考虑像素点的纵坐标,使用横坐标作为输入值,用三角函数来做计算:
3取整
随机数是一串离散的点,所以我们对输入的x值取整:
y = sin(floor(x));
这时,我们已经有了一个大致的算法,能够获取一串随机数,并且不像cpu算法那样,需要等待上一个计算结果,能够直接根据当前的x值得出随机数。
这里的y值,在数学逻辑上是保证了绝对不会有周期性的
为什么?
4少许优化
给三角函数加上倍数系数和固定系数,可以调节振幅和频率。
通过加一个取小数部分的操作,可以让这个算法看起来更随机一点,并且将取值范围从-a,a变成0,a。这种优化没有固定公式,可以随意组合。
离散点之间过度
使用上面的这种算法,可以获得一系列由像素点坐标来确定的离散随机数值。放到2维图中,可以制作成噪点图。
但是这种噪点图和我们想要的噪声图有一些差距。原因便是噪声图虽然是随机的,但是相邻的像素点之间的值是接近的,也就是说,噪声图存在一个平滑过度的关系。
所以,我们不以像素点作为单位来取噪点,使用较大的间隔获取噪点,然后在噪点之间做平滑过度。
平滑过度的计算一般使用shader中内置的smoothstep函数即可。
很多细致的shader作品中,会使用更符合作品需求的平滑计算函数,如果你看到一个shader代码中,有类似这种算式,不用头疼,基本上是在构造它自己的平滑公式。不确定的可以放进一些能展示数学函数图像的软件里,可以获得和smoothstep类似的图像。
2维图像
取噪点,将一副图网格化。对一个晶格的四个顶点做平滑过度,一张2维的随机图像就生成了。
在使用fbm前,我们先看看之前得到的图像,明显细节不够丰富。
什么是分形?
fbm是通过叠加更改了振幅和频率后的图像来丰富细节的。每次迭代由于公式的形式不变,只变了振幅和频率,因此借用音乐中的概念,将其成为一个“八度”(octave)
在unity中生成噪声图,并保存为图片。
unity中渲染图像,如果只是在编辑器中使用,可以用computeshader,能够方便地借助gpu进行高并发计算。
创建computershader:
computeShader代码:
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain
// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> Result;
float random (float2 full_uv) {
return frac(sin(dot(full_uv.xy,
float2(12.9898,78.233)))*
43758.5453123);
}
float noise (in float2 full_uv) {
float2 i = floor(full_uv);
float2 f = frac(full_uv);
// Four corners in 2D of a tile
float a = random(i);
float b = random(i + float2(1.0, 0.0));
float c = random(i + float2(0.0, 1.0));
float d = random(i + float2(1.0, 1.0));
float2 u = f * f * (3.0 - 2.0 * f);
return lerp(a, b, u.x) +
(c - a)* u.y * (1.0 - u.x) +
(d - b) * u.x * u.y;
}
#define OCTAVES 6
float fbm (in float2 full_uv) {
// Initial values
float value = 0.0;
float amplitude = .5;
float frequency = 0.;
//
// Loop of octaves
for (int i = 0; i < OCTAVES; i++) {
value += amplitude * noise(full_uv);
full_uv *= 2.;
amplitude *= .5;
}
return value;
}
[numthreads(8,8,1)]
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
Result[id.xy] = float4(fbm(float2(float(id.x), float(id.y) / 32)), 0, 0, 1);
}
脚本中调用computeShader修改RenderTexture纹理的数据,并保存为图片:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.UI;
public class noiseCreator : MonoBehaviour
{
public ComputeShader cs;
private RenderTexture rt;
// Start is called before the first frame update
void Start()
{
int kernelHandle = cs.FindKernel("CSMain");
int width = 1024;
int height = 1024;
rt = new RenderTexture(width ,height, 0, RenderTextureFormat.RFloat);
rt.enableRandomWrite = true;
rt.Create();
cs.SetTexture(kernelHandle, "Result", rt);
// cs.Dispatch(kernelHandle, width / 8, height / 8, 1);
cs.Dispatch(kernelHandle, width / 8, height / 8, 1);
GetComponent<RawImage>().material.mainTexture = rt;
}
public void save() {
Texture2D t = new Texture2D(rt.width, rt.height, TextureFormat.R8, false);
var previous = RenderTexture.active;
RenderTexture.active = rt;
t.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
RenderTexture.active = previous;
t.Apply();
var bytes = t.EncodeToTGA();
System.IO.File.WriteAllBytes("fbm.tga", bytes);
}
}