본문 바로가기

유니티/Catlikecoding

[의사난수노이즈] 해시 - 시각화(Catlikecoding)

반응형

 튜토리얼에 포함된 내용이 아니고 제가 추가한 내용은 밑줄로 표시했습니다.

 


 

 사물을 예측할 수 없고 다양하며 자연스럽게 보이게 하기 위해서는 무작위성이 필요합니다. 이것이 정말로 무작위인지 아니면 정보 부족이나 관찰자의 이해로 인해 그렇게 나타나는지는 중요하지 않습니다. 그래서 이 무작위성을 결정적이며 완전한 무작위가 아닌 것으로 만들 수 있습니다. 잘못 설계된 다중 스레드 코드는 경쟁 조건(race conditions)으로 이어져 예측할 수 없는 결과를 초래할 수 있지만, 이는 신뢰할 수 있는 임의성의 소스가 아닙니다. 진정하게 신뢰할 수 있는 무작위성은 일반적으로 사용할 수 없는 대기 잡음을 샘플링하는 하드웨어와 같은 외부 소스에서만 얻을 수 있습니다.

 

 진정한 무작위성은 일반적으로 바람직하지 않습니다. 그것에 의해 생성된 모든 것은 재현할 수 없는 일회성 이벤트입니다. 결과는 매번 다를 것입니다. 이상적으로는 특정 입력에 대해 고유하고 고정된 무작위 출력을 생성하는 프로세스가 있습니다. 이것이 해쉬 함수의 용도입니다.

 

 이 자습서에서는 작은 큐브의 2D 그리드를 만들고 이를 사용하여 해시 함수를 시각화합니다. 기본 시리즈(Cat like coding의 basics 시리즈)에 설명된 대로 새 프로젝트를 생성하여 시작하세요. 우리는 job systems를 사용할 것이므로 burst 패키지를 가져옵니다. URP도 사용하므로 Universal RP 에셋을 생성하고 이를 사용하도록 Unity를 구성합니다.

 

 

1. 해시 작업

using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;

using static Unity.Mathematics.math;

public class HashVisualization : MonoBehaviour {

	[BurstCompile(FloatPrecision.Standard, FloatMode.Fast, CompileSynchronously = true)]
	struct HashJob : IJobFor {

		[WriteOnly]
		public NativeArray<uint> hashes;
        
		public void Execute(int i) {
			hashes[i] = (uint)i;
		}
	}
}

 

 해시 값은 본질적으로 고유한 의미가 없는 비트 모음입니다. 일반적인 데이터인 32비트(4바이트) 패킷에 가장 가까운 유형을 사용할 것입니다. 처음에는 job's의 실행 인덱스를 해시 값으로 직접 사용합니다.

 

	static int
		hashesId = Shader.PropertyToID("_Hashes"),
		configId = Shader.PropertyToID("_Config");

	[SerializeField]
	Mesh instanceMesh;

	[SerializeField]
	Material material;

	[SerializeField, Range(1, 512)]
	int resolution = 16;

	NativeArray<uint> hashes;

	ComputeBuffer hashesBuffer;

	MaterialPropertyBlock propertyBlock;

 

 인스턴스 Mesh 및 material에 대한 구성 옵션과 해상도 슬라이더를 추가하여 필요한 NativeArray, ComputeBuffer, MaterialPropertyBlock을 스크립트에 추가할 것입니다. 버퍼의 셰이더 식별자에 _Hashes를 사용하고 추가 구성을 위해 _Config 셰이더 속성을 추가합니다.

 

	void OnEnable () {
		int length = resolution * resolution;
		hashes = new NativeArray<uint>(length, Allocator.Persistent);
		hashesBuffer = new ComputeBuffer(length, 4);

		new HashJob {
			hashes = hashes
		}.ScheduleParallel(hashes.Length, resolution, default).Complete();

		hashesBuffer.SetData(hashes);

		propertyBlock ??= new MaterialPropertyBlock();
		propertyBlock.SetBuffer(hashesId, hashesBuffer);
		propertyBlock.SetVector(configId, new Vector4(resolution, 1f / resolution));
	}

 

OnEnable에서 모든 항목을 초기화합니다.

 

( 튜토리얼에 있는 코드 그대로 가져왔지만 유니티 오브젝트의 경우 저런 식의 널병합 연산자 사용은 fake null로 인해 위험할 수 있습니다. )

 

	void OnDisable () {
		hashes.Dispose();
		hashesBuffer.Release();
		hashesBuffer = null;
	}

	void OnValidate () {
		if (hashesBuffer != null && enabled) {
			OnDisable();
			OnEnable();
		}
	}
	void Update () {
		Graphics.DrawMeshInstancedProcedural(
			instanceMesh, 0, material, new Bounds(Vector3.zero, Vector3.one),
			hashes.Length, propertyBlock
		);
	}

 

 OnDisable에서 해시 및 버퍼를 정리하고, OnValidate에서 재설정하는 방법을 사용하여 Run 모드에서 구성을 변경하면 그리드가 새로 고쳐집니다. 마지막으로 Update에서 그리기 명령을 내려주기만 하면 됩니다.

 

2. 셰이더

#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)
	StructuredBuffer<uint> _Hashes;
#endif

float4 _Config;

void ConfigureProcedural () {
	#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)
		float v = floor(_Config.y * unity_InstanceID);
		float u = unity_InstanceID - _Config.x * v;
		
		unity_ObjectToWorld = 0.0;
		unity_ObjectToWorld._m03_m13_m23_m33 = float4(
			_Config.y * (u + 0.5) - 0.5,
			0.0,
			_Config.y * (v + 0.5) - 0.5,
			1.0
		);
		unity_ObjectToWorld._m00_m11_m22 = _Config.y;
	#endif
}

'procedural configuration function'이 포함된 hlsl 파일을 만듭니다. Basics시리즈와의 차이점은 인스턴스 식별자에서 인스턴스의 위치를 직접 도출한다는 것입니다. 1차원의 선(앞에서 초기화한 1차원 NativeArray을 다룰 인덱스)은 동일한 길이의 세그먼트로 잘라서 2차원의 그리드로 변환됩니다. 우리는 식별자를 분해능으로 나눕니다. GPU에는 정수 나눗셈이 없으므로 플로어 함수를 통해 나눗셈의 분수 부분만 삭제합니다. 그러면 2차원 좌표를 얻을 수 있습니다. 이에 v라는 이름을 붙일 것입니다. 그 다음 식별자에서 v를 빼서 u좌표를 구합니다. 그런 다음 uv좌표를 사용하여 인스턴스를 xz 평면에 배치하고, 원점에서 단위 큐브 안에 있도록 오프셋 및 스케일링을 합니다.

 

float3 GetHashColor () {
	#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)
		uint hash = _Hashes[unity_InstanceID];
		return _Config.y * _Config.y * hash;
	#else
		return 1.0;
	#endif
}

또한 해시를 사용하여 RGB색상을 생성하는 함수를 소개합니다. 처음에는 해시를 해상도의 제곱으로 나누는 회색조 값으로 만들어 해시 인덱스를 기반으로 검은색에서 흰색 사이의 색상을 생성합니다.

 

void ShaderGraphFunction_float (float3 In, out float3 Out, out float3 Color) {
	Out = In;
	Color = GetHashColor();
}

void ShaderGraphFunction_half (half3 In, out half3 Out, out half3 Color) {
	Out = In;
	Color = GetHashColor();
}

 

그리고 셰이더 그래프를 위한 함수를 만들고 이를 셰이더 그래프에 연결해줍니다. 아래 'InjectPragmas'란 이름의 커스텀 노드를 추가하여 아래 그림과 같이 셰이더 노드를 구성해줍니다.

 

#pragma instancing_options assumeuniformscaling procedural:ConfigureProcedural
#pragma editor_sync_compilation

Out = In;

 

 

이 셰이더 그래프는 unlit shader graph를 이용하여 제작하였습니다.

 

 

빈 게임 오브젝트에 위에서 제작한 스크립트를 넣고, Mesh에는 유니티 기본 Cube, Material에는 방금 제작한 셰이더 그래프의 메터리얼을 넣어주면 위와 같은 오브젝트가 생성됩니다.

 

위 프로그램은 대부분 잘 작동하는 것 처럼 보이지만, 일부 해상도에서는 위와 같이 그리드가 잘못 정렬됩니다. 이 오류는 부동 소수점 정밀도 제한으로 인해 발생합니다. 어떤 경우에는 floor를 적용하기 전 정수보다 약간 작은 값으로 끝나기 때문에 인스턴스가 잘못 배치됩니다. 소수점을 버리기 전에 0.00001정도의 편향을 추가하여 이 문제를 해결할 수 있습니다.

 

		float v = floor(_Config.y * unity_InstanceID + 0.00001);

 

3. 패턴

		uint hash = _Hashes[unity_InstanceID];
		return (1.0 / 255.0) * (hash & 255);

진정한 해시함수를 구현하기 전에 간단한 수학적 함수에 대해 간단히 살펴보겠습니다. 첫 번째로 현재 그레이스케일 그라데이션이 256포인트마다 반복되도록 하겠습니다. 이 작업은 GetHashColor에서 최하위 8개의 해시 비트만 고려함으로 구현할 수 있습니다. 이것은 &(AND) 연산자를 통해 10진수 255인 이진수 11111111과 해시를 결합하여 수행됩니다. 이렇게 하면 값이 마스킹되어 8개의 최하위 비트만 유지되어 0-255 범위로 제한됩니다. 그런 다음 해당 범위를 255로 나누어 0-1로 스케일링 할 수 있습니다.

 

 

			hashes[i] = (uint)(frac(i * 0.381f) * 256f);

 

결과 패턴은 분해능에 따라 달라집니다. 이런 명백한 그라디언트를 Weyl 시퀀스로 바꾸어 봅시다. C# 스크립트인 HashJob의 Excute로 캐스팅 하기 전 아래와 같은 수식으로 0에서 255 사이의 해시를 얻을 수 있습니다.

 

 

추가설명

Weyl 시퀀스는 불연속적인 균일 분포를 생성하는 데 자주 사용됩니다

frac함수는 n의 소수 부분, 즉 소수점 뒤에 있는 부분을 반환하는 함수입니다.

 

		public int resolution;

		public float invResolution;
        
		public void Execute(int i) {
			float v = floor(invResolution * i + 0.00001f);
			float u = i - resolution * v;
			hashes[i] = (uint)(frac(u * v * 0.381f) * 255f);
		}
		new HashJob {
			hashes = hashes,
			resolution = resolution,
			invResolution = 1f / resolution
		}.ScheduleParallel(hashes.Length, resolution, default).Complete();

 

지금까지 해상도에 의존하는 방향으로 반복적인 그라디언트가 생성되었습니다. 해상도와 무관하게 만들려면 인덱스 대신 포인트의 UV 좌표를 기반으로 기능을 설정해야 합니다. 셰이더에서와 같은 방식으로 jobs에서 좌표를 찾을 수 있습니다. 그런 다음 U와 V의 곱을 시퀀스의 기초로 사용합니다. 이를 위해서는 해상도와 그 역수에 대한 필드를 추가해야 합니다. 위 코드와 같이 job을 수정하고, OnEnable에서 작업에 필요한 데이터를 전달해줍니다.

 

 

이제 우리는 이전보다 더 임의적으로 보이지만 분명한 반복이 있는 흥미로운 패턴을 얻었습니다! 이제 더 나은 결과를 얻으려면 좋은 해시 함수가 필요합니다.

 

 

 

 

Hashing

A Unity C# Pseudorandom Noise tutorial about creating a small version of xxHash and visualizing hashes.

catlikecoding.com

반응형