游戏开发中的噪声算法

 一、噪声


噪声是游戏编程的常见技术,广泛应用于地形生成,图形学等多方面。

那么为什么要引入噪声这个概念呢?在程序中,我们经常使用直接使用最简单的rand()生成随机值,但它的问题在于生成的随机值太“随机”了,得到的值往往总是参差不齐,如下图使用随机值作为像素点的黑白程度:

而使用噪声,我们得到的值看起来虽然随机但平缓,这种图也看起来更自然和舒服:

1.1 随机性


随机性是噪声的基础,不必多说。

1.2 哈希性


在《Minecraft》里,由于世界是无限大的,它以“Chunk”区块(16×16×256格子)为单位,只加载玩家附近的区块。也就是说,当玩家在移动时,它会卸载远离的区块,然后加载靠近的区块。

一个问题是,当玩家离开一个区块时,进入第二个区块,然后又回到第一个区块,此时玩家期望看到的第一个区块和之前看到的保持一致。例如,输入1时得到0.3,输入2时得到0.7,当再次输入1时预期得到0.3。

因此噪声的一个重要性质是哈希性(可哈希的)。

 尽管使用输入值作为srand()的参数来设置rand()的种子,从而达到哈希效果也是可行的。
 然而最好花点时间写一个自己的哈希函数,使其简易使用而且也不破坏程序其他地方使用rand()的效果。

//一个随机性的哈希函数
unsigned int hash11(int position){
const unsigned int BIT_NOISE1 = 0x85297A4D;
const unsigned int BIT_NOISE2 = 0x68E31DA4;
const unsigned int BIT_NOISE3 = 0x1B56C4E9;
unsigned int mangled = position;
mangled *= BIT_NOISE1;
mangled ^= (mangled >> 8);
mangled += BIT_NOISE2;
mangled ^= (mangled << 8);
mangled *= BIT_NOISE3;
mangled ^= (mangled >> 8);
return mangled;
}

1.3 平滑性

对一个随机生成地形来说,如果简单的使用随机和哈希组合,
那么容易得到下图(以一维地图举例,x轴为位置,y轴为地形高度):

容易看出的问题是,由于随机的杂乱无章,地形非常的参差不齐,这可不是一个自然的地形。

我们期望得到的地形不仅随机还应该是平滑的,这样才显得自然,如下图:

为了达到连续性,自然想到利用插值函数进行插值,常见的插值方法有:线性插值、缓和曲线插值


二、Value噪声

Value噪声是最简单的一种噪声,其主要思路是定义若干个顶点且每个顶点含有一个随机值(以顶点坐标作为参数通过哈希运算得到的),该随机值会周围坐标产生影响,越靠近顶点则越容易受该顶点影响(输出值越接近顶点随机值)。当需要求某个坐标的输出值时,需要将该坐标附近的各个顶点所造成的影响值进行叠加,从而得到一个总值并输出之。

原理:

1.首先定义一个晶格结构,每个晶格的顶点有一个伪随机值(Value)。对于二维的Value噪声来说,晶格结构就是一个平面网格(通常是正方形),三维的就是一个立体网格(通常是正方体)。

2.输入一个点(二维空间的话就是2D坐标),我们找到它所在晶格的顶点(二维下有4个,三维下有8个,N维下有2^n个),并经过哈希运算得到这些顶点的伪随机值。

3.根据这些顶点的伪随机值,使用插值函数计算出输入点的输出值。对于插值函数的权重,我们还需要使用缓和曲线(ease curves)来计算这些伪随机值的权重和。在原始的Perlin噪声实现所使用的缓和曲线是s(t) = 3t^2 - 2t^3,在2002年的论文中Perlin又改进为s(t) = 6t^5-15t^4+10t^3

实现:

int valueNoise(Vector2 p){
  //晶格以1为长度单位,通过向下取整可以确定p点所在晶格
  //注意:不应使用转变整型,因为负数的转整型是向上取整,而正数则是向下取整,这可能会导致(-1~0)和(0~1)的边缘问题
  Vector2 pi = Vector2(floor(p.x),floor(p.y));
  //找到对应晶格的四个顶点坐标
  Vector2 vertex[4] = {{pi.x,pi.y},{pi.x+1,pi.y},{pi.x,pi.y+1},{pi.x+1,pi.y+1}};
  //通过hash21函数得出坐标对应的随机值
  float vertexRandom[4] = {{hash21(vertex[0])},{hash21(vertex[1])},{hash21(vertex[2])},{hash21(vertex[3])}};  
  //wx、wy代表p点的权重,实际就是以(0.0~1.0)的范围表示在晶格中的位置比例
  float wx = (p.x-pi.x))/1.0f;
  float wy = (p.y-pi.y))/1.0f;
  //插值
  return interpolation(wx,wy,vertexRandom);
}

三、柏林噪声

谈起噪声,最著名的且最常用的莫过于Perlin噪声,Perlin噪声的名字来源于它的创始人Ken Perlin。

在理解了上面Value噪声后,我们再来看看柏林噪声的主要想法:
定义若干个顶点且每个顶点含有一个随机梯度向量,这些顶点会根据自己的梯度向量对周围坐标产生势能影响,沿着顶点的梯度方向越上升则势能越高。当需要求某个坐标的输出值时,需要将该坐标附近的各个顶点所造成的势能进行叠加,从而得到一个总势能并输出之。

我们给顶点赋予一个随机性的哈希函数,输入一个坐标可以得到一个随机向量,满足上述随机性和哈希性。
此外,由于势能是沿着梯度方向渐变的,所以很容易得到平滑性。

原理:

和Value噪声一样,它也是一种基于晶格的噪声,也需要三个步骤:

1.首先定义一个晶格结构,每个晶格的顶点有一个随机的梯度向量。对于二维的Perlin噪声来说,晶格结构就是一个平面网格(通常是正方形),三维的就是一个立体网格(通常是正方体)

2.输入一个点(二维空间的话就是2D坐标),我们找到它所在晶格的顶点(二维下有4个,三维下有8个,N维下有2^n个),并经过哈希运算得到这些顶点的梯度向量(随机向量);接着计算该点到各个晶格顶点的距离向量,再分别与顶点代表的梯度向量做点乘,得到2^n个梯度值结果

//点乘
float dot(Vector2 v1,Vector2 v2){
  return v1.x*v2.x+v1.y*v2.y;
}

//求梯度值(本质是求顶点代表的梯度向量与距离向量的点积)
float grad(Vector2 vertex, Vector2 p)
{
  return dot(hash22(vertex), p);
}

3.使用缓和曲线来计算它们的权重和(同样的,可以是s(t) = 3t^2 - 2t^3,也可以是s(t) = 6t^5-15t^4+10t^3

下图通过颜色差异显示了由2D柏林噪声生成的各像素点的值:

实现:

//二维柏林噪声
float perlinNoise(Vector2 p)
{  
  //向量两个纬度值向下取整
  Vector2 pi = Vector2(floor(p.x),floor(p.y));
  //找到对应晶格的四个顶点坐标
  Vector2 vertex[4] = {{pi.x,pi.y},{pi.x+1,pi.y},{pi.x,pi.y+1},{pi.x+1,pi.y+1}};
  //通过grad函数得出坐标对应的随机值
  float vertexRandom[4] = {grad(vertex[0],p),grad(vertex[1],p),grad(vertex[2],p),grad(vertex[3],p)};  
  //wx、wy代表p点的权重,实际就是以(0.0~1.0)的范围表示在晶格中的位置比例
  float wx = (p.x-pi.x))/1.0f;
  float wy = (p.y-pi.y))/1.0f;
  //插值
  return interpolation(wx,wy,vertexRandom);
}

gard函数另一个更快的实现方式,它与标准实现方式的区别是:晶体顶点是从若干个梯度向量里随机选择一个向量而不是产生一个随机向量,这样做可以预先计算好求梯度值时各项的系数。因此我们只需这样重写一下grad函数:

//求梯度值(本质是求顶点代表的梯度向量与距离向量的点积)
float grad(Vector2 vertex, Vector2 p)
{
    switch(hash21(vertex) % 4)
    {
      case 1: return  p.x + p.y;  //代表梯度向量(1,1)
      case 2: return -p.x + p.y;  //代表梯度向量(-1,1)
      case 3: return  p.x - p.y;  //代表梯度向量(1,-1)
      case 4: return -p.x - p.y;  //代表梯度向量(-1,-1)
      default: return 0; // never happens
    }
}

这里示例提供了4个可选的随机向量,实际上这个数量是偏少的,如果想要更加多样的效果,建议在实现时多提供些可选的随机向量。

四、Simplex噪声

Simplex噪声也是一种基于晶格的梯度噪声,它和Perlin噪声在实现上唯一不同的地方在于,它的晶格并不是方形(在2D下是正方形,在3D下是立方体,在更高纬度上我们称它们为超立方体,hypercube),而是单形(simplex)。

通俗解释单形的话,可以认为是在N维空间里,选出一个最简单最紧凑的多边形,让它可以平铺整个N维空间。我们可以很容易地想到一维空间下的单形是等长的线段,把这些线段收尾相连即可铺满整个一维空间。在二维空间下,单形是三角形,我们可以把等腰三角形连接起来铺满整个平面。三维空间下的单形就是四面体。更高维空间的单形也是存在的。

总结起来,在n维空间下,超立方体的顶点数目是2^n,而单形的顶点数目是n+1,这使得我们在计算梯度噪声时可以大大减少需要计算的顶点权重数目。

一个潜在的问题是如何找到输入点所在的单形。
在计算Perlin噪声时,判断输入点所在的正方形是非常容易的,我们只需要对输入点下取整即可找到。
对于单形来说,我们需要对单形进行坐标偏斜(skewing),把平铺空间的单形变成一个新的网格结构,这个网格结构是由超立方体组成的,而每个超立方体又由一定数量的单形构成:

我们之前讲到的单形网格如上图中的红色网格所示,它们有一些等边三角形组成(注意到这些等边三角形是沿空间对角线排列的)。经过坐标倾斜后,它们变成了后面的黑色网格,这些网格由正方形组成,每个正方形是由之前两个等边三角形变形而来的三角形组成。这个把N维空间下的单形网格变形成新网格的公式如下:

x' = x + (x+y+...)\cdot K1

y' = y + (x+y+...)\cdot K1

其中, K1 = \frac{\sqrt{n+1}-1}{n}

在二维空间下,取n为2即可。这样变换之后,我们就可以按照之前方法判断该点所在的超立方体,在二维下即为正方形。

原理:

1.坐标偏斜:把输入点坐标进行坐标偏斜。

x' = x + (x+y+...)\cdot K1

y' = y + (x+y+...)\cdot K1

2.找到顶点:对偏斜后坐标下取整得到输入点所在的超立方体xi = floor(x'), yi = floor(y'),...我们还可以得到小数部分xf = x'-xi, yf = y'-yi,...我们把之前得到的(xf,yf,...)中的数值按降序排序,来决定输入点位于变形后的哪个单形内。这个单形的顶点是由按序排列的(0, 0, …, 0)到(1, 1, …, 1)中的n+1个顶点组成,共有n!种可能性。
我们可以按下面的过程来得到这n+1个顶点:从零坐标(0, 0, …, 0)开始,找到当前最大的分量,在该分量位置加1,直至添加了所有分量。这一步的算法复杂度即为排序复杂度O(n^2)

3.梯度选取:我们在偏斜后的超立方体网格上获取该单形的各个顶点的伪随机梯度向量。

4.变换回单形网格里的顶点:我们首先需要把单形顶点变回到之前由单形组成的单形网格。这一步需要使用第一步公式的逆函数来求得:

x = x' + (x'+y'+...)\cdot K2

y = y' + (x'+y'+...)\cdot K2

其中, K2 = \frac{\frac{1}{\sqrt{n+1}}-1}{n}

5.贡献度取和:我们由此可以得到输入点到这些单形顶点的位移向量。这些向量有两个用途,一个是为了和顶点梯度向量点乘,另一个是为了得到之前提到的距离值dist,来据此求得每个顶点对结果的贡献度:

(r^2 - |dist|^2)^4 \times dot(dist,grad)

实现:

float simplexNoise(Vector2 p)
{
  const float K1 = 0.366025404; // (sqrt(3)-1)/2;
  const float K2 = 0.211324865; // (3-sqrt(3))/6;
  //坐标偏斜
  float s = (p.X + p.Y) * K1;
  Vector2 pi = Vector2(floor(p.X+s),floor(p.Y+s));
  float t = (pi.X + pi.Y) *K2;
  Vector2 pf = p-(pi-t*Vector2::UnitVector);
  Vector2 vertex2Offset = (pf.X < pf.Y) ? Vector2(0, 1) : Vector2(1, 0);
  
  //顶点变换回单行网格空间
  Vector2 dist1 = pf;
  Vector2 dist2 = pf - vertex2Offset + K2 * Vector2::UnitVector;
  Vector2 dist3 = pf - Vector2(1,1) + 2 * K2 * Vector2::UnitVector;

  //计算贡献度取和
  float hx = 0.5f - Vector2::DotProduct(dist1, dist1);
  float hy = 0.5f - Vector2::DotProduct(dist2, dist2);
  float hz = 0.5f - Vector2::DotProduct(dist3, dist3);

  hx=hx*hx*hx*hx;
  hy=hy*hy*hy*hy;
  hz=hz*hz*hz*hz;

  //结果范围是[-1,1]
  return 70*(
  		hx*Vector2::DotProduct(dist1, hash22(pi)) 
	  	+hy*Vector2::DotProduct(dist2, hash22(pi + vertex2Offset))
	  	+hz*Vector2::DotProduct(dist3, hash22(pi + Vector2(1,1)))
      );
}

虽然理解上Simplex噪声相比于Perlin噪声更难理解,但由于它的效果更好、速度更优,因此很多情况下会替代Perlin噪声。

而且高维的噪声并不少见,例如对于常见的二维噪声纹理,我们可以额外引入时间分量,变成一个2D纹理动画(三维噪声),用于火焰纹理动画等..
对于常见的三维噪声纹理,引入额外的时间分量,就可以变成一个3D纹理动画(四维噪声),用于3D云雾动画等..
当我们需要一个可循环无缝衔接的动画时(见下文可平埔的噪声),那噪声又要提高一个维度。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/333801.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

X-Bogus加密参数分析与jsvmp算法(仅供学习)

文章目录 1. 抓包分析2. X-Bogus参数分析 【作者主页】&#xff1a;吴秋霖 【作者介绍】&#xff1a;Python领域优质创作者、阿里云博客专家、华为云享专家。长期致力于Python与爬虫领域研究与开发工作&#xff01; 【作者推荐】&#xff1a;对JS逆向感兴趣的朋友可以关注《爬虫…

【算法Hot100系列】跳跃游戏

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老导航 檀越剑指大厂系列:全面总结 jav…

【题解 Trie树 字符串】 C - New but Nostalgic Problem

题目描述&#xff1a; 分析&#xff1a; 题目中涉及到了若干字符串的公共前缀&#xff0c;显然可以用trie树去完成 建立trie树的同时&#xff0c;我们为了做题方便&#xff0c;用以下两个数组去记录一下trie树的信息&#xff1a; t o t i tot_i toti​表示以i为根的子树中有几…

ICBE 2024第十二届深圳国际跨境电商交易博览会

ICBE 2024第十二届深圳国际跨境电商交易博览会 暨中国跨境电商综试区发展高峰论坛 展会时间&#xff1a;2024年9月2日-4日 展会地点&#xff1a;深圳会展中心&#xff08;福田&#xff09; 指导单位:广东省商务厅 主办单位&#xff1a;广东省电子商务协会/扩展集团 承办单…

基于局部信息提取的人脸标志检测算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 人脸检测 4.2 局部区域选择 4.3 特征提取 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 .........................................…

【LeetCode每日一题】2171. 拿出最少数目的魔法豆

2024-1-18 文章目录 [2171. 拿出最少数目的魔法豆](https://leetcode.cn/problems/removing-minimum-number-of-magic-beans/)思路&#xff1a; 2171. 拿出最少数目的魔法豆 思路&#xff1a; 对输入的数组进行排序&#xff0c;使得数组中的元素按照升序排列。初始化一个变量s…

python如何包含其他路径的模块

python 包含其他路径的模块&#xff1a; 例如目录结构: dir1 |__ init.py |__ module1.py dir2 |__ main.py main.py from dir1 import module1首先需要在 dir1 添加 init.py 文件&#xff0c;该文件可以是空文件。 其次需要将dir1 的父目录添加到python 解释器的&#xf…

小红书投放策略有哪些?品牌运营思路

想要在小红书进行合理的达人投放&#xff0c;离不开一份完备且具备可实施性的达人投放策略。今天我们和大家分享下小红书投放策略有哪些&#xff1f;品牌运营思路&#xff01; 制定小红书达人投放策略&#xff0c;按照以下四个步骤进行即可。 1、投放目的 这里的确定投放目的包…

跟着pink老师前端入门教程-day06

十一、CSS 的背景 通过CSS背景属性&#xff0c;可以给页面元素添加背景样式 背景属性可以设置背景颜色、背景图片、背景平铺、背景图片位置、背景图像固定等。 11.1 背景颜色 background-color 属性定义了元素的背景颜色 一般情况下元素背景颜色默认值是transparent&…

KubeSphere平台使用

KubeSphere官网地址&#xff1a;https://kubesphere.io/zh/ KubeKey一键部署K8S集群&#xff1a;https://kubesphere.io/zh/docs/v3.4/installing-on-linux/introduction/multioverview/ 一台master node&#xff08;初始化主节点&#xff09;、两台 work node&#xff08; joi…

2024-01-15(SpringMVCMybatis)

1.拦截器&#xff1a;如果我们想在多个handler方法(controller中的方法)执行之前或者之后都进行一些处理&#xff0c;甚至某些情况下需要拦截掉&#xff0c;不让handler方法执行&#xff0c;那么就可以使用SpringMVC为我们提供的拦截器。 拦截器和过滤器的区别&#xff1a;过滤…

浏览器插件:Web Scraper 基本用法和抓取页面内容(无需写代码,即可爬取数据)

Web Scraper 是一个浏览器扩展&#xff0c;用于从页面中提取数据(网页爬虫)。对于简单或偶然的需求非常有用&#xff0c;例如正在写代码缺少一些示例数据&#xff0c;使用此插件可以很快从类似的网站提取内容作为模拟数据。从 Chrome 的插件市场安装后&#xff0c;页面 F12 打开…

Python项目——搞怪小程序

1、介绍 使用python编写一个小程序&#xff0c;回答你是猪吗。 点击“是”提交&#xff0c;弹窗并退出。 点击“不是”提交&#xff0c;等待5秒&#xff0c;重新选择。 并且隐藏了关闭按钮。 2、实现 新建一个项目。 2.1、设计UI 使用Qt designer设计一个UI界面&#xff0c…

[bat]0基础实现自动化办公-新建bat脚本文件

一、引言 本文是自动化办公之路的开篇&#xff0c;主要面向0基础同学介绍如何新建一个bat脚本文件。接下来会逐渐深入讲解如何实现自动化办公&#xff0c;如有什么需求场景&#xff0c;可评论区留言&#xff0c;我后面会逐一实现。 二、方案 通过对text文本文档文件改文件后…

DETR 个人理解

DETR 个人理解 目录 DETR 个人理解 概念说明 transformer网络结构 整体流程 损失计算 整体理解 结果说明 论文 代码 参考链接 个人拙见&#xff0c;仅供参考&#xff0c;欢迎指正交流 这篇论文还是挺重要的&#xff0c;因为是transforms用于目标检测的第一篇论文&am…

一、Linux基础

一、Linux 1.1 Linux 的应用领域 1.1.1 个人桌面领域的应用 此领域是 Linux 比较薄弱的环节但是随着发展&#xff0c;近几年 linux 在个人桌面领域的占有率在逐渐提高 1.1.2 服务器领域 linux 在服务器领域的应用是最高的 linux 免费、稳定、高效等特点在这里得到了很好的…

OpenGL:关于渲染窗口在主屏和扩展屏上纹理贴图不一致的问题

自己写了一个例子&#xff0c;将图像纹理贴图到窗口&#xff0c;并且可以设置窗口的起始位置。 原始图像如下 当设置渲染窗口在主屏时&#xff0c;渲染的结果如下 没什么问题。 但是当设置窗口显示在扩展屏时&#xff0c;效果如下 可以看出纹理没有显示完整 网上找一下&…

Spring Boot整合Druid(druid 和 druid-spring-boot-starter)

引言 在现代的Web应用开发中&#xff0c;高性能的数据库连接池是确保应用稳定性和响应性的关键因素之一。Druid是一个开源的高性能数据库连接池&#xff0c;具有强大的监控和统计功能&#xff0c;能够在Spring Boot应用中提供出色的数据库连接管理。本文将研究在Spring Boot中…

【双端队列】【维护单调队列】Leetcode 239 滑动窗口最大值【难】

【双端队列】Leetcode 239 滑动窗口最大值 双端队列的操作解法1 利用双端队列实现单调队列 ---------------&#x1f388;&#x1f388;题目链接 Leetcode 239 滑动窗口最大值&#x1f388;&#x1f388;------------------- 双端队列的操作 创建双端队列&#xff1a;Deque<…

解决字符串类型转数字类型相加结果异常问题

js字符串类型转换数字类型有七种方法&#xff0c;分别是parseInt()&#xff0c;parseFloat()&#xff0c;Math.floor()&#xff0c;乘以数字&#xff08;*1&#xff09;&#xff0c;Number()&#xff0c;双波浪号 (~~number)&#xff0c;一元运算符&#xff08;number&#xff…