=> GuardHei

Illusory Walls Ahead

F+ or Deferred? Which one should I choose?


Here we go again!

代码生成Perlin噪点图

概述

最近在弄的游戏项目需要一些渲染特效,所以在弄shader。大部分实现复杂特效的shader都需要一个甚至多个噪点图进行随机采样。但网上往往下不到合适的噪点图,而且也不可能一张噪点图从头用到尾,所以我就翻了翻官方文档,弄了一个自动生成Perlin噪点图的编辑器拓展。

PS: 这期内容有点水,因为最近都在忙this-is-ib这个项目,好不容易抽点时间写篇博客(一月两更可不能忘🌚)。

思路

基本思路是使用Mathf.PerlinNoise(float x, float y)方法获得特定uv坐标的噪点值,转换为灰度写进纹理,然后将这个纹理保存在本地。我也懒得说废话了,直接上代码吧。

实现

public static void GeneratePerlinNoiseMap(float scale) {
		string path = EditorUtility.SaveFilePanelInProject("生成柏林噪点图", "perlin_noise_texture_" + scale + "x", "png", "保存", Application.dataPath + "/Resources/Textures");
		if (!string.IsNullOrEmpty(path)) {
			if (EditorUtility.DisplayCancelableProgressBar("生成柏林噪点图", "初始化", 0f)) {
				ClearProgressBar();
				return;
			}
			int size = 256;
			int sizeSqr = size * size;
            Texture2D texture2D = new Texture2D(size, size);
			float oX = Random.value;
			float oY = Random.value;
			for (int i = 0; i < size; i++) {
				for (int j = 0; j < size; j++) {
					float greyScale = Mathf.PerlinNoise(oX + ((float) i) / ((float) size) * scale, ((float) j) / ((float) size) * scale);
					texture2D.SetPixel(i, j, new Color(greyScale, greyScale, greyScale));
					if (j % 100 == 0) {
						if (EditorUtility.DisplayCancelableProgressBar("生成柏林噪点图", greyScale.ToString(), (float) (size * i + j + 1) / sizeSqr)) {
							ClearProgressBar();
							return;
						}
					}
				}
			}
			texture2D.Apply();
			File.WriteAllBytes(path, texture2D.EncodeToPNG());
			EditorUtility.ClearProgressBar();
			AssetDatabase.ImportAsset(path.Substring(path.IndexOf("Assets")));
		}
	}

代码写得稍微有点魔幻,但也不是不能看😂,逐行分析吧。首先方法内第一句string path = EditorUtility.SaveFilePanelInProject("生成柏林噪点图", "perlin_noise_texture_" + scale + "x", "png", "保存", Application.dataPath + "/Resources/Textures");是调出保存文件的窗口,选择即将创建的噪点图保存位置。要注意的是最后一个参数是默认路径,得确保是确实存在的(没有就手动创建一个呗)。

然后就进入了一个大的if分支。这个是对上一部获取到的保存路径进行有效性检验。如果使用者(比如某策划)后悔进行了这个操作,在保存文件的窗口选择了取消,那返回的路径就是无效的。我们用string.IsNullOrEmpty(string toTest)进行有效性判断。

下面一行是调用编辑器的可取消进度条显示当前操作进度,接受一下返回值判断使用者有没有点击取消操作。我之所以用可取消进度条是因为之前作死弄了一个10000x10000分辨率的纹理操作,然后编辑器就卡死了😂,不得已只能强退。

好了,终于开始正式的内容了。我们先声明一下噪点纹理的大小,我准备都用256x256分辨率。这个分辨率在我目前的项目里精度足够了,而且大小也还好。然后用sizeSqr缓存一下总像素个数(用来在后面显示操作进度,不重要)。根据size数据创建2D纹理对象。

接下来的两行可能有些令人迷惑。简单点来说,oXoY是采样的起始点,通过UnityEngine.Random.value产生随机的效果。因为一张uv图的坐标范围是在(0, 0)到(1, 1)之间的,所以直接用UnityEngine.Random.value返回的就是一个[0, 1]之间的随机浮点数。

继续看下去,进入了一个嵌套的for循环。对,你猜得没错,现在开始采样噪点并写入我们的纹理的时候了。由于Perlin噪点图是灰度图,没有彩色数据。而我们知道,对于一个用rgb法表示的颜色,如果[r, g, b]每个值都相等的话,得到的就是一个白色到黑色间的颜色。由此我们把使用Mathf.PerlinNoise(float x, float y)的采样结果,一个float对象做为当前颜色所有的部分(rgb)的值大小。在Unity中,Color结构体的rgb也是[0, 1]间的浮点数,而不是常见的[0, 256]的整形。值得一提的是,Mathf.PerlinNoise返回的float在大部分的情况下会在[0, 1]区间,但是官方文档特地指出是有可能大于1的。当然了,我们这里就不用对结果clamp了,因为Color的构造函数会自动把大于1的参数强制调到1。

那么使用Mathf.PerlinNoise(float x, float y)采样的坐标参数怎么确定呢?对于每一个方向(即xy),我们从起点开始,加上当前遍历到的像素的相对坐标(当前位置/该方向长度,即i / size,因为要得到一个浮点结果,所以强转一下((float) i / (float) size))。当然了我还乘了一个传进来的参数scale。这是啥呢?简单来说,scale控制了噪点的密集程度,越大越密集,相似的噪点图案重复的越多,因为采样的偏移更快了。

得到值后,texture2D.SetPixel(i, j, new Color(greyScale, greyScale, greyScale))把结果写入我们的纹理中。接下来的代码是显示操作进度的,每绘制100个像素更新一下进度条。之所以不每像素更新是因为效率考量,想想看256x256一共有多少个像素,每次更新GUI的开销不小。同时记得如果点了操作取消就退出这个方法。

最后最后,采样结束后,text2D.Apply()保存写入的数据。然后使用File.WriteAllBytes(string path, byte[] data)将纹理写入纹理。我们使用.png的编码方式,所以还要在用texture2D.EncodeToPNG()来获得对应格式的二进制数据。

这样,操作主体结束了!EditorUtility.ClearProgressBar()移除进度条。最后,使用AssetDatabase.ImportAsset(string path)将这个纹理导入到项目里,这样就可以在编辑器里看到它了(不然得手动刷新项目才行)。这里得注意一下,传入的路径不是基于操作系统的,而是从项目的资源目录开始的(Assets/…),所以我把之前的保存路径截取一下传进去。

封装

为了方便使用,我们需要让这个功能可以在编辑器里面调用。在Editor文件下新建一个脚本MaterialTool.cs,内容如下:

using System.IO;
using UnityEditor;
using UnityEngine;
using Random = UnityEngine.Random;

public static class MaterialTool {

	[MenuItem("材质工具/生成噪点图/Perlin 1X")]
	public static void GeneratePerlinNoiseMap1X() {
		GeneratePerlinNoiseMap(1);
	}
	
	[MenuItem("材质工具/生成噪点图/Perlin 2X")]
	public static void GeneratePerlinNoiseMap2X() {
		GeneratePerlinNoiseMap(2);
	}
	
	[MenuItem("材质工具/生成噪点图/Perlin 3X")]
	public static void GeneratePerlinNoiseMap3X() {
		GeneratePerlinNoiseMap(3);
	}
	
	[MenuItem("材质工具/生成噪点图/Perlin 4X")]
	public static void GeneratePerlinNoiseMap4X() {
		GeneratePerlinNoiseMap(4);
	}
	
	[MenuItem("材质工具/生成噪点图/Perlin 5X")]
	public static void GeneratePerlinNoiseMap5X() {
		GeneratePerlinNoiseMap(5);
	}
	
	[MenuItem("材质工具/生成噪点图/Perlin 6X")]
	public static void GeneratePerlinNoiseMap6X() {
		GeneratePerlinNoiseMap(6);
	}
	
	[MenuItem("材质工具/生成噪点图/Perlin 7X")]
	public static void GeneratePerlinNoiseMap7X() {
		GeneratePerlinNoiseMap(7);
	}
	
	[MenuItem("材质工具/生成噪点图/Perlin 8X")]
	public static void GeneratePerlinNoiseMap8X() {
		GeneratePerlinNoiseMap(8);
	}
	
	[MenuItem("材质工具/生成噪点图/Perlin 9X")]
	public static void GeneratePerlinNoiseMap9X() {
		GeneratePerlinNoiseMap(9);
	}
	
	public static void GeneratePerlinNoiseMap(float scale) {
		string path = EditorUtility.SaveFilePanelInProject("生成柏林噪点图", "perlin_noise_texture_" + scale + "x", "png", "保存", Application.dataPath + "/Resources/Textures");
		if (!string.IsNullOrEmpty(path)) {
			if (EditorUtility.DisplayCancelableProgressBar("生成柏林噪点图", "初始化", 0f)) {
				ClearProgressBar();
				return;
			}
			int size = 256;
			int sizeSqr = size * size;
            Texture2D texture2D = new Texture2D(size, size);
			float oX = Random.value;
			float oY = Random.value;
			for (int i = 0; i < size; i++) {
				for (int j = 0; j < size; j++) {
					float greyScale = Mathf.PerlinNoise(oX + ((float) i) / ((float) size) * scale, ((float) j) / ((float) size) * scale);
					texture2D.SetPixel(i, j, new Color(greyScale, greyScale, greyScale));
					if (j % 100 == 0) {
						if (EditorUtility.DisplayCancelableProgressBar("生成柏林噪点图", greyScale.ToString(), (float) (size * i + j + 1) / sizeSqr)) {
							ClearProgressBar();
							return;
						}
					}
				}
			}
			texture2D.Apply();
			File.WriteAllBytes(path, texture2D.EncodeToPNG());
			EditorUtility.ClearProgressBar();
			AssetDatabase.ImportAsset(path.Substring(path.IndexOf("Assets")));
		}
	}
}

OK,大功告成!

改进

你难道不觉得给每种密集程度的噪点图写个编辑器菜单拓展很傻吗?可以直接写一个自定义的EditorWindow提供更便捷的操作。

Recent Articles

Unity优化——渲染与画面(一)

概述 游戏做出来后结果只能跑20-30fps无疑是令人悲伤的,所以游戏优化显得非常重要。最近我也在对这就是IB这个项目进行优化(虽然项目才在一半不到的开发进度),有了不少感慨,这里可以拿出来探讨探讨。我真的有必要优化吗?优化是一个费时费力的事(大部分情况下),于是不少开发者只要看到运行效率只要不是不能接受就忽视了优化这个环节。某种意义上来说,过优化确实是不必要以及有害的,会大大地降低了开发效率。但是在很多情况下,虽然目前表现尚可,但由于代码安排得不合理,很有可能在后续的开发过程...…

UnityContinue
Earlier Articles

Unity中输入系统的设计

概述 游戏,作为一种交互式艺术,玩家的操作是必不可少的。事实上,对于游戏中操作系统的设计也是非常耗费精力的。本篇博文不是从游戏设计角度上来叙述怎样设计游戏的操作,而是从程序设计的角度提出一种更通用和抽象的输入系统。PS:本篇内容特别多,本来是想拆成两篇的,但考虑到内容的连贯,还是放到一篇里解决。Unity的输入系统作为一个非常强大的游戏引擎,Unity已经为开发者提供了一层高级的输入抽象。大部分的开发我们都使用UnityEngine.Input类来解决问题。Input类提供了诸如按键检测...…

UnityContinue