Boat Attack 데모 - Wave (1)

Like Big·2022년 7월 19일
0

Boat Attack Demo

목록 보기
3/3

Wave 구현 방법

Jobs를 이용한 게임 오브젝트 Wave

Boat Attack Water System/Scripts/Water.cs - init, clean

/* CPU side call from OnEnable, 파도에 관한 각종 속성 값을 세팅*/
if(GerstnerWavesJobs.Initialized == false && Application.isPlaying)
                GerstnerWavesJobs.Init();

/* CPU side call from OnDisable */                
if(Application.isPlaying) GerstnerWavesJobs.Cleanup();

Boat Attack Water System/Scripts/GerstnerWavesJobs(거스너).cs - job 실행

  • 유니티 내장 Math 패키지 using
  • NNativeContainer란?
    NativeContainer는 네이티브 메모리에 상대적으로 안전한 C# 래퍼를 제공하는 관리되는 값 타입입니다. 여기에는 관리되지 않는 할당에 대한 포인터가 들어 있습니다. Unity C# 잡 시스템과 함께 NativeContainer를 사용하면 잡이 복사본으로 작업하는 것이 아니라 메인 스레드와 공유되는 데이터에 액세스할 수 있습니다. 이용할 수 있는 NativeContainer 타입으로 NativeArray라고... (출처 : 유니티 Document)
  • Allocator.Persistent : 가장 느린 할당. Memory에 계속유지.
  • Allocator.Temp : 가장 빠른 할당. Job 생성하고 Job 끝나기전 메인스레드에서 기다리게 하는 작업, 1 프레임 이하 작업에 적합.
  • Allocator.TempJob : 중간 빠른 할당. Job 생성하고 Job 끝나기 대기하지 않으면서 4프레임 Job이 끝날때 사용이 적합.
using Unity.Jobs;
using Unity.Mathematics; // 유니티의 Mathmatics 패키지 설치 필요 

namespace WaterSystem
{
    public static class GerstnerWavesJobs
    {
        private static NativeArray<Wave> _waveData;
        private static NativeArray<float3> _positions;
        private static NativeArray<float3> _wavePos;
        private static NativeArray<float3> _waveNormal;
        private static JobHandle _waterHeightHandle;
        
        /* 
          하나의 NativeArray에 계산되어야할 모든 게임 오브젝트들(각 group으로 나눠짐)이 담기며
          (first int)=guid, guid 따른 그룹 인덱스 (second int2)=offset으로 관리 (x는 시작 인덱스, y는 끝 인덱스)
        */
        static readonly Dictionary<int, int2> Registry = new Dictionary<int, int2>();

        public static void Init()
        {
            // Water.cs 에서 설정된 Wave 데이터를 사용
            _waveCount = Water.Instance._waves.Length; 
            _waveData = new NativeArray<Wave>(_waveCount, Allocator.Persistent);
            for (var i = 0; i < _waveData.Length; i++)
            {
                _waveData[i] = Water.Instance._waves[i];
            }

			/* 
              넉넉히 4096 크기의 메모리 할당, position은 게임오브젝트 좌표 인풋
              wavePos, waveNormal은 계산완료된 아웃풋 데이터
            */
            _positions = new NativeArray<float3>(4096, Allocator.Persistent);
            _wavePos = new NativeArray<float3>(4096, Allocator.Persistent);
            _waveNormal = new NativeArray<float3>(4096, Allocator.Persistent);
        }

        public static void Cleanup()
        {
            /* Jobs 종료 */
            _waterHeightHandle.Complete();
            
            /* native arrays 해제는 Dispose 함수 */           		
            _waveData.Dispose();
            기타 array ...
        }

		// 계산 되어야할 게임오브젝트를 추가하는 작업
        public static void UpdateSamplePoints(ref NativeArray<float3> samplePoints, int guid)
        {
            //새로 Job을 시작하기전 JobHandler 종료.
            CompleteJobs();

			//Job 시작전 _positions에 다시 최신의 좌표값을 넣는 함수.
			
			// guid가 존재하면 offset 그대로 사용. 
            if (Registry.TryGetValue(guid, out var offsets))
            {
                for (var i = offsets.x; i < offsets.y; i++)
                    _positions[i] = samplePoints[i - offsets.x];
            }
            
            // guid가 가 없으면 Registry offset 추가 및 _positionCount+
            // 제한사항 : guid의 samplePoint는 처음 한꺼번에 add되면 추가로 나중에 add될 수 없다. Offset이 이미 고정됨.
            else
            {
            	// 크기 4096넘지 않으면 add
                if (_positionCount + samplePoints.Length >= _positions.Length) return; 
                
                offsets = new int2(_positionCount, _positionCount + samplePoints.Length);
                Registry.Add(guid, offsets);
                _positionCount += samplePoints.Length;
            }
        }
        
        private static void CompleteJobs()
        {
            if (_firstFrame || _processing == false) return;
            
            _waterHeightHandle.Complete();
            _processing = false;
        }

		// 연산된 결과를 반환, guid로 offset 확인후 x~y 인덱스까지 가져옴.
        public static void GetData(int guid, ref float3[] outPos, ref float3[] outNorm)
        {
            if (!Registry.TryGetValue(guid, out var offsets)) return;
            
            _wavePos.Slice(offsets.x, offsets.y - offsets.x).CopyTo(outPos);

            if(outNorm != null)
                _waveNormal.Slice(offsets.x, offsets.y - offsets.x).CopyTo(outNorm);
        }


        // Height jobs for the next frame
        public static void UpdateHeights()
        {
            if (_processing) return;
            _processing = true;

            // JobStruct를 생성, 아래 생성자에서 멤버 Array 변수 할당
            var waterHeight = new GerstnerWavesJobs.HeightJob()
            {
                WaveData = _waveData,
                Position = _positions,
                OffsetLength = new int2(0, _positions.Length),
                Time = Time.time,
                OutPosition = _wavePos,
                OutNormal = _waveNormal
            };
            
            /*
              추후 _waterHeightHandle.complete를 위해 할당. 
              4096 이 아닌 _positionCount 만큼 void Execute(int i) i : 0 ~ _positionCount iterator
              32 => innerloopBatchCount로 32개의 i만큼을 한번에 하나의 쓰레드에서 처리.
            */
            _waterHeightHandle = waterHeight.Schedule(_positionCount, 32);
            
            // JobBatch 실행... 생성된 waterHeight를 직접 실행하지 않음.
            JobHandle.ScheduleBatchedJobs();

            _firstFrame = false;
        }

        [BurstCompile] //?
        private struct HeightJob : IJobParallelFor
        {
            [ReadOnly] public NativeArray<Wave> WaveData; 
            [ReadOnly] public NativeArray<float3> Position;

            [WriteOnly] public NativeArray<float3> OutPosition;
            [WriteOnly] public NativeArray<float3> OutNormal;
            [ReadOnly] public float Time;
            [ReadOnly] public int2 OffsetLength;

            /* Job을 이용한 병열 처리로  아래 여러 웨이브를 수학적 중첩으로 계산 */
            public void Execute(int i)
            {
                if (i < OffsetLength.x || i >= OffsetLength.y - OffsetLength.x) return;
                
                var waveCountMulti = 1f / WaveData.Length;
                var wavePos = new float3(0f, 0f, 0f);
                var waveNorm = new float3(0f, 0f, 0f);

                for (var wave = 0; wave < WaveData.Length; wave++)
                {
                    var pos = Position[i].xz; //x와 z만으로 계산됨

					// Wave data vars
                    var amplitude = WaveData[wave].amplitude;
                    var direction = WaveData[wave].direction;
                    var wavelength = WaveData[wave].wavelength;
                    var omniPos = WaveData[wave].origin;
                    
                    // 수학적 계산은 코드를 참고
                    ... 
                }
                OutPosition[i] = wavePos;
                OutNormal[i] = math.normalize(waveNorm.xzy);
            }
        }
    }
}

실행 방법

  • 그룹을 관리하는 스크립트를 만들고 그 스크립트에 각 게임오브젝트를 스크립트에 넣고. _samplePoints에 할당.
    (개별 게임 오브젝트에 스크립트를 전부 붙이는게 아닌 그룹으로 관리하기 때문)
  • Heights 미리 선언, ref 활용하여 복사하지 않고 포인터 전달, 해당 그룹의 guid(유니크한 int)도 인자로 전달
  • GetData로 결과 Heighs와 Normals를 받아와 기본적인 물체의 속도와 잘 합쳐 자연스런 최종 결과 좌표 세팅.
private NativeArray<float3> _samplePoints;
GerstnerWavesJobs.UpdateSamplePoints(ref _samplePoints, _guid);
GerstnerWavesJobs.GetData(_guid, ref Heights, ref _normals);

쉐이더에서 메시 버텍스 Wave

continue...

profile
개고운(개발,고양이,운동)

0개의 댓글