본문 바로가기

Unity/DOTS

Unity - UnityPhysicsSamples 살펴보기 #1 Hello World/a. Hello World

반응형

참고자료 : https://github.com/Unity-Technologies/EntityComponentSystemSamples
해당예제 장면 별 간단 설명 

 

Unity-Technologies/EntityComponentSystemSamples

Contribute to Unity-Technologies/EntityComponentSystemSamples development by creating an account on GitHub.

github.com

이 챕터에선 위 자료 중 UnityPhysicsSamples만 살펴볼것입니다. ECSSamples를 원한다면 아래 링크를 봐주세요.

2020/08/03 - [Unity/DOTS] - Unity - ECSSamples 살펴보기 #1 - /HelloCube의 ForEach와 IJobChunk

 

Unity - ECSSamples 살펴보기 #1 - /HelloCube의 ForEach와 IJobChunk

참고자료 : https://github.com/Unity-Technologies/EntityComponentSystemSamples Unity-Technologies/EntityComponentSystemSamples Contribute to Unity-Technologies/EntityComponentSystemSamples developmen..

javart.tistory.com

주의사항 

 1. 번역에 100% 신뢰를 갖고 읽지 마세요. 조금이라도 이상하면 원문을 보고 다시 확인하시는 것을 추천드립니다.

 2. 작성자는 이 예제를 적용하면서 수차례 프리징을 겪었습니다.

 

작업환경

 OS : Windows 10

 Editor : 2019.4.6f1

 RP : Standard

 

참고사항

 ECSSamples에서 URP로 잘 되었었지만 이 예제는 적용 후 프리징이 너무 잦아 Standard RP로 적용해봅니다.

 

설정시 주의사항

 1. 에디터 먼저 설정하지 않으면 똑같이 한 것 같아도 문제가 해결되지 않는 경우가 있습니다. 사전에 설정을 하시고 디렉토리를 설정해주세요.

 2. 에디터 설정은 최소한으로 했습니다. 하나라도 안하면 동작에 문제가 생기거나 플레이 자체가 안될 수 있습니다.

 

에디터 설정하기

 1. Player Settings/Other Settings/

    -> Api Compatibillity Level -> .NET 4.x 으로 설정하세요.

    -> Allow 'unsafe' Code -> 활성화 해주세요. *unsafe를 사용하는 스크립트가 예제에 내장되어 있습니다.

 2. Window/Package Manager 열기

 3. Entities 설치 (이 것을 통해 DOTS 설치가 한번에 됩니다.)

 4. Unity Physics 설치(예제를 보시면 왠지 아시겠죠?) *Havok은 예제에 포함되어 있지 않습니다. 예제와의 충돌 예방을 위해 추가 설치는 지양해주세요.

 5. Input System 설치 (예제에서 사용한 스크립트가 포함되어 있었습니다.)

  -> 설치 중 나오는 "새 Input System을 적용하기 위해 에디터를 재시작하시겠습니까?"는 아니오를 누릅니다.

  -> 설치가 완료 된후 Player Settings/Other Settings/Active Input Handling -> Both로 설정해주세요.

      * 일부 조작 스크립트는 기존 인풋을 사용합니다. 따라서 둘 다 사용으로 충돌을 회피합니다.

      * 조작시 버스트가 요구한 에디터 재시작과 충돌해서 에디터가 꺼질 수 있습니다만, 다시 켜면 작동합니다.

 6. Hybrid Renderer 설치 (Unity.Rendering 을 사용한 스크립트가 있습니다.)

 

 

디렉토리 설정

 1. {DownloadPath}\EntityComponentSystemSamples-master\EntityComponentSystemSamples-master\ECSSamples UnityPhysicsSamples에 있는 Assets를 ECSSampleUnityPhysicsSample로 변경

 2. 작업할 프로젝트 Assets에 복사

 3. Assets/Common 디렉토리 삭제 (예제에 있는 InputActions.cs와 겹칩니다.)

 

예제 설명을 보는 방법

 1. 주로 스크립트나 문서를 기준으로 작성될 예정입니다. 따라서, 예제를 같이 설정하고 같이 보지 않으면 큰 의미가 없습니다.

 2. 스크립트를 포함한 리소스의 경우 HelloCube와 달리 Common으로 공통 리소스 분류 디렉토리가 있으므로 경로를 기준으로 표시할 예정입니다. 예) /Common/Scripts/RayTracer -> 공통 분류된 스크립트 RayTracer.cs

UnityPhysicsSamples

 1a. Hello World

해당 Scene에서는 별도로 물리연산에 들어간 스크립트가 따로 있진 않습니다. 자체적으로 주어진 물리연산용 컴포넌트가 있으니 장면(Scene)을 확인해주세요.

 

1a의 장면 구성요소

 

 - RayTracing 적용하기 : 

 RT가 적용된 모습을 보고싶다면 Physics Scene Basic/Main Camera/RayTraceCamera 게임 오브젝트와 RayTraceUI도 활성화 해두셔야 합니다.

 고화질의 RT를 제공하는 것은 아니나 화면 우측상단에 간단하게 표시할 수 있도록 구성된 것 같습니다.

 

RT가 적용된 Game View 화면

 

- Common/Scripts/RayTracer/RayTracer : 

 #Job 시스템을 이용해 간단히 작성된 RT기도 하고, 이 예제를 학습하는 주 목적이 DOTS 물리연산을 중심으로 되어있는 만큼 간단하게만 살펴보겠습니다.

 

using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
using Unity.Entities;

namespace Unity.Physics.Extensions
{
    public class RayTracer : MonoBehaviour
    {
        GameObject imageDisplay;
        UnityEngine.Material blasterMaterial;
        Texture2D blasterTexture;

        public bool AlternateKeys = false;
        //
        public bool CastSphere = false;
        //그림자 보일지
        public bool Shadows = false;
        public float ImagePlane = 10.0f;
        public float RayLength = 100.0f;
        //SSAO를 대체한 RT 기반 AO의 정도입니다.
        public float AmbientLight = 0.2f;
        //Inspector에서 보시면 아시겠지만 생성될 UI 위치를 나타냅니다.
        public GameObject DisplayTarget;
        //RT된 이미지 해상도입니다. 늘림에 따라 뚜렷해지지만 그만큼 무거워지기도하고, 캔버스 크기가 작아서 큰 티가 안납니다.
        int ImageRes = 100;
        //생선된 기본 Plane의 절반 범위
        float planeHalfExtents = 5.0f; 

        //같은 경로에 작성되어 있는 RayTracerSystem
        RayTracerSystem.RayResult lastResults;
        bool ExpectingResults;

        private void OnDisable()
        {
            if (ExpectingResults)
            {
                lastResults.PixelData.Dispose();
                ExpectingResults = false;
            }
        }

        // Start is called before the first frame update
        void Start()
        {
            // Creates a y-up plane
            imageDisplay = GameObject.CreatePrimitive(PrimitiveType.Plane);

            if (DisplayTarget != null)
            {
                imageDisplay.transform.parent = DisplayTarget.transform;
            }
            else
            {
                imageDisplay.transform.parent = gameObject.transform;
            }

            imageDisplay.GetComponent<MeshRenderer>().shadowCastingMode = ShadowCastingMode.Off;

            // For 2019.1: // blasterTexture = new Texture2D(ImageRes, ImageRes, UnityEngine.Experimental.Rendering.GraphicsFormat.R32G32B32A32_UInt , UnityEngine.Experimental.Rendering.TextureCreationFlags.None);
            blasterTexture = new Texture2D(ImageRes, ImageRes);
            blasterTexture.filterMode = FilterMode.Point;

            blasterMaterial = new UnityEngine.Material(imageDisplay.GetComponent<MeshRenderer>().materials[0]);
            blasterMaterial.shader = Shader.Find("Unlit/Texture");
            blasterMaterial.SetTexture("_MainTex", blasterTexture);
            imageDisplay.GetComponent<MeshRenderer>().materials = new[] { blasterMaterial };

            // Orient our plane so we cast along +Z:
            imageDisplay.transform.localRotation = Quaternion.AngleAxis(-90.0f, new Vector3(1, 0, 0));
            imageDisplay.transform.localPosition = Vector3.zero;
            imageDisplay.transform.localScale = Vector3.one;
        }

        // Update is called once per frame
        private void Update()
        {
            Vector3 imageCenter = transform.TransformPoint(new Vector3(0, 0, -ImagePlane));

            if (ExpectingResults)
            {
                NativeStream.Reader reader = lastResults.PixelData.AsReader();
                for (int i = 0; i < lastResults.PixelData.ForEachCount; i++)
                {
                    reader.BeginForEachIndex(i);
                    while (reader.RemainingItemCount > 0)
                    {
                        int x = reader.Read<int>();
                        int y = reader.Read<int>();
                        Color c = reader.Read<Color>();
                        blasterTexture.SetPixel(x, y, c);
                    }
                    reader.EndForEachIndex();
                }

                blasterTexture.Apply();
                lastResults.PixelData.Dispose();
                ExpectingResults = false;
            }

            if (BasePhysicsDemo.DefaultWorld == null)
            {
                return;
            }

            RayTracerSystem rbs = BasePhysicsDemo.DefaultWorld.GetExistingSystem<RayTracerSystem>();
            if (rbs == null || !rbs.IsEnabled)
            {
                return;
            }

            Vector3 lightDir = new Vector3(0, 0, -1);
            GameObject sceneLight = GameObject.Find("Directional Light");
            if (sceneLight != null)
            {
                lightDir = sceneLight.transform.rotation * lightDir;
            }

            Vector3 up = transform.rotation * new Vector3(0, 1, 0);
            Vector3 right = transform.rotation * new Vector3(1, 0, 0);

            lastResults = rbs.AddRequest(new RayTracerSystem.RayRequest
            {
                PinHole = transform.position,
                ImageCenter = imageCenter,
                Up = up,
                Right = right,
                LightDir = lightDir,
                RayLength = RayLength,
                PlaneHalfExtents = planeHalfExtents,
                AmbientLight = AmbientLight,
                ImageResolution = ImageRes,
                AlternateKeys = AlternateKeys,
                CastSphere = CastSphere,
                Shadows = Shadows,
                CollisionFilter = CollisionFilter.Default
            });
            ExpectingResults = true;
        }
    }
}

 

- /Common/Scripts/CameraController

 간단하게 작성된 카메라 조작 스크립트입니다.

 

카메라 축과 용어

출처 : https://stackoverrun.com/ko/q/11546172

 

using UnityEngine; 

public class CameraControl : MonoBehaviour
{
    public float lookSpeedH = 2f;
    public float lookSpeedV = 2f;
    public float zoomSpeed = 2f;
    public float dragSpeed = 5f;

    private float yaw;
    private float pitch;

    
    private void Start()
    {
        // x - right    pitch
        // y - up       yaw
        // z - forward  roll
        yaw = transform.eulerAngles.y;
        pitch = transform.eulerAngles.x;
    }

    void Update()
    {
        if (!enabled) return;

        if (Input.touchCount > 0)
        {
            float touchToMouseScale = 0.25f;
            // look around with first touch
            // 첫번 째 터치를 기준으로 둘러봅니다.
            Touch t0 = Input.GetTouch(0);
            yaw += lookSpeedH * touchToMouseScale * t0.deltaPosition.x;
            pitch -= lookSpeedV * touchToMouseScale * t0.deltaPosition.y;
            transform.eulerAngles = new Vector3(pitch, yaw, 0f);

            // and if have extra touch, also fly forward
            // 두번째 터치가 있다면, 앞으로 날아갑니다.
            if (Input.touchCount > 1)
            {
                Touch t1 = Input.GetTouch(1);
                Vector3 offset = new Vector3(t1.deltaPosition.x, 0, t1.deltaPosition.y);
                transform.Translate(offset * Time.deltaTime * touchToMouseScale, Space.Self);
            }
        }
        else
        {
            //Look around with Right Mouse
            //마우스 오른쪽 버튼을 기준으로 둘러봅니다.
            if (Input.GetMouseButton(1))
            {
                yaw += lookSpeedH * Input.GetAxis("Mouse X");
                pitch -= lookSpeedV * Input.GetAxis("Mouse Y");

                transform.eulerAngles = new Vector3(pitch, yaw, 0f);

                Vector3 offset = Vector3.zero;
                float offsetDelta = Time.deltaTime * dragSpeed;
                if (Input.GetKey(KeyCode.LeftShift)) offsetDelta *= 5.0f;
                if (Input.GetKey(KeyCode.S)) offset.z -= offsetDelta;
                if (Input.GetKey(KeyCode.W)) offset.z += offsetDelta;
                if (Input.GetKey(KeyCode.A)) offset.x -= offsetDelta;
                if (Input.GetKey(KeyCode.D)) offset.x += offsetDelta;
                if (Input.GetKey(KeyCode.Q)) offset.y -= offsetDelta;
                if (Input.GetKey(KeyCode.E)) offset.y += offsetDelta;

                transform.Translate(offset, Space.Self);
            }

            //drag camera around with Middle Mouse
            //마우스 중간(휠 누르기)을 이용해 드래그 할 수 있습니다.
            if (Input.GetMouseButton(2))
            {
                transform.Translate(-Input.GetAxisRaw("Mouse X") * Time.deltaTime * dragSpeed, -Input.GetAxisRaw("Mouse Y") * Time.deltaTime * dragSpeed, 0);
            }

            //Zoom in and out with Mouse Wheel
            //휠을 이용해 줌을 적용할 수 있습니다.
            transform.Translate(0, 0, Input.GetAxis("Mouse ScrollWheel") * zoomSpeed, Space.Self);
        }
    }
}

 

- Common/Scripts/MousePick/MousePickBehaviour : 

엔티티를 마우스로 잡고 움직일 수 있게 해주는 스크립트입니다. 분석해보면서 느낀바(정확하지 않을 수 있습니다.)는 다음과 같습니다.

 

 1. 이 (모노비헤비어 에서의)컴포넌트를 갖고 있는 게임 오브젝트가 변환 될 때 MousePick 컴포넌트 데이터를 적용

 2. MousePick 컴포넌트를 갖고 있는 엔티티가 있을 시에만 계산

 3. MousePickSystem에서 먼저 마우스의 입력에 따라 물리 캐스팅

 4. MouseSpringSystem에서 물리 캐스팅된 정보를 바탕으로 작성된 스프링을 이용해 강체에 힘 적용

using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Transforms;
using UnityEngine;
using UnityEngine.Assertions;
using static Unity.Physics.Math;

namespace Unity.Physics.Extensions
{
    public class ColliderUtils
    {
        //전달된 정보에 따라 해당 콜라이더가 트리거인지를 판단합니다.
        [BurstCompile]
        public static bool IsTrigger(BlobAssetReference<Collider> collider, ColliderKey key)
        {
            bool bIsTrigger = false;
            unsafe
            {
                var c = (Collider*)collider.GetUnsafePtr();
                {
                    var cc = ((ConvexCollider*)c);
                    if (cc->CollisionType != CollisionType.Convex)
                    {
                        c->GetLeaf(key, out ChildCollider child);
                        cc = (ConvexCollider*)child.Collider;
                        Assert.IsTrue(cc->CollisionType == CollisionType.Convex);
                    }
                    bIsTrigger = cc->Material.CollisionResponse == CollisionResponsePolicy.RaiseTriggerEvents;
                }
            }
            return bIsTrigger;
        }
    } //Class ColliderUtils

    // A mouse pick collector which stores every hit. Based off the ClosestHitCollector
    // 히트된 모든 것을 저장하는 ClosestHitController 기반의  마우스 픽 컨트롤러입니다.
    // ICollector<T>의 설명이 메뉴얼에 자세히 안나와 있지만  3개의 변수와 AddHit 메소드를 구현하도록 되어있으니 살펴봅시다.
    [BurstCompile]
    public struct MousePickCollector : ICollector<RaycastHit>
    {
        
        //ICollector<T>가 말하는 변수는 이 구간의 변수가 아닙니다.
        public bool IgnoreTriggers; //트리거 무시할 것인지
        public NativeArray<RigidBody> Bodies; //저장된 강체들, 여기선 모든 강체 리스트입니다.(컴포넌트 데이터 배열)
        public int NumDynamicBodies; //다이나믹 강체 갯수

        //이 구간의 3개의 변수입니다.
        public bool EarlyOutOnFirstHit => false;
        public float MaxFraction { get; private set; }
        public int NumHits { get; private set; }

        //주의하셔야할 것은 UnityEngine.RaycastHit가 아닌 Unity.Physics.RaycastHit 임을 인지하셔야 합니다.
        //가장 가까운(최근의) 히트 정보
        private RaycastHit m_ClosestHit;
        public RaycastHit Hit => m_ClosestHit;

        //생성자입니다.
        public MousePickCollector(float maxFraction, NativeArray<RigidBody> rigidBodies, int numDynamicBodies)
        {
            m_ClosestHit = default(RaycastHit);
            MaxFraction = maxFraction;
            NumHits = 0;
            IgnoreTriggers = true;
            Bodies = rigidBodies;
            NumDynamicBodies = numDynamicBodies;
        }

        #region ICollector
        //ICollector<T>에서 요구하는 메소드입니다.
        public bool AddHit(RaycastHit hit)
        {
            //해당 조건이 참인지 확인합니다. 참이 아니어도 중단점이 되진 않고, 예외를 통해 로깅합니다.
            //hit.Fraction은 0~1의 소수점으로 히트가 발생된 거리의 비율입니다.
            Assert.IsTrue(hit.Fraction < MaxFraction);

            //처리해도 될지에 대해 구합니다.
            var isAcceptable = (hit.RigidBodyIndex >= 0) && (hit.RigidBodyIndex < NumDynamicBodies);
            //트리거를 무시할지에 따라 처리 가능 여부를 다시 판단합니다. 당연히 트리거라면 처리 불가로 판정됩니다.
            if (IgnoreTriggers)
            {
                var body = Bodies[hit.RigidBodyIndex];
                isAcceptable = isAcceptable && !ColliderUtils.IsTrigger(body.Collider, hit.ColliderKey);
            }

            //처리 불가시 리턴을 통해 취소합니다.
            if (!isAcceptable)
            {
                return false;
            }

            //최대 거리비율을 현 비율로 변환합니다.
            MaxFraction = hit.Fraction;
            //히트 정보 갱신
            m_ClosestHit = hit;
            //히트 갯수 초기화
            NumHits = 1;
            return true;
        }

        #endregion

    }

    //마우스픽에 관한 컴포넌트데이터
    public struct MousePick : IComponentData
    {
        public int IgnoreTriggers;
    }

    //실제 인스펙터에 등록될 MonoBehaviour와 엔티티 변환 인터페이스
    public class MousePickBehaviour : MonoBehaviour, IConvertGameObjectToEntity
    {
        //트리거를 무시할 것인지
        public bool IgnoreTriggers = true;

        //변환 메소드
        void IConvertGameObjectToEntity.Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
        {
            //이 오브젝트가 엔티티로 변환되면서 MousePick 컴포넌트 데이터를 추가함에 따라 이 엔티티에서 마우스 픽 관련 작업을 함을 예측할 수 있습니다.
            dstManager.AddComponentData(entity, new MousePick()
            {
                IgnoreTriggers = IgnoreTriggers ? 1 : 0,
            });
        }
    }

    // Attaches a virtual spring to the picked entity
    // 선택된 엔티티를 가상의 스프링에 부착합니다.
    //UpdateAfter와 UpdateBefore에 대한 설명은 자세히 안나오지만 대략적 유추는 가능한 것 같습니다.
    //BuildPhysicsWorld(피직스월드가 생성된?) 이후와 EndFramePhysicsSystem(물리시스템의 프레임의 끝) 이전 (and 조건 혹은 or 조건으로)
    [UpdateAfter(typeof(BuildPhysicsWorld)), UpdateBefore(typeof(EndFramePhysicsSystem))]
    public class MousePickSystem : SystemBase
    {
        //최대거리
        const float k_MaxDistance = 100.0f;

        EntityQuery m_MouseGroup;
        BuildPhysicsWorld m_BuildPhysicsWorldSystem;

        public NativeArray<SpringData> SpringDatas;
        public JobHandle? PickJobHandle;

        public struct SpringData
        {
            public Entity Entity;
            public int Dragging; // bool isn't blittable
            public float3 PointOnBody;
            public float MouseDepth;
        }


        [BurstCompile]
        struct Pick : IJob
        {
            [ReadOnly] public CollisionWorld CollisionWorld;
            [ReadOnly] public int NumDynamicBodies;
            public NativeArray<SpringData> SpringData;
            public RaycastInput RayInput;
            public float Near;
            public float3 Forward;
            [ReadOnly] public bool IgnoreTriggers;

            //작업이 실행 될 때
            public void Execute()
            {
                //물리 캐스트에 따라 해당 정보를 저장하기 위한 구조체를 생성합니다. 최대 거리 비율 1, 충돌세계의 강체목록, 다이나믹 강체 갯수
                var mousePickCollector = new MousePickCollector(1.0f, CollisionWorld.Bodies, NumDynamicBodies);
                //트리거를 무시할지에 대한 정보 동기화
                mousePickCollector.IgnoreTriggers = IgnoreTriggers;

                //실제 레이캐스트 인풋에 맞춰 계산 후 mousePickCollector에 저장합니다.
                //이것도 메뉴얼에 안적혀있어서 나름 유추해보자면
                // CastRay(RaycastInput, <T>) 메소드로 <T> 는 Collector 계열입니다.
                // 캐스트 후 CastRay 내부에서 MousePickCollector에서 AddHit(RaycastHit)를 호출는 것 같습니다.
                // 간단한 근거로 AddHit 최하단에선 처리 가능으로 판단 시 최대거리비율을 갱신하도록 되어있고, 이 아래 코드에선 1 미만일 경우 처리하도록 되어있습니다.
                CollisionWorld.CastRay(RayInput, ref mousePickCollector);
                if (mousePickCollector.MaxFraction < 1.0f)
                {
                    //처리된 결과에 따라 정보를 불러옵니다.
                    float fraction = mousePickCollector.Hit.Fraction;
                    RigidBody hitBody = CollisionWorld.Bodies[mousePickCollector.Hit.RigidBodyIndex];

                    //또 설명없이 메뉴얼만 있습니다.
                    //수학적 트랜스폼이란것 같습니다. float3*3, float3의 형태나 quaternion, float3의 형태나 RigidTransform을 받을 수 있도록 되어있습니다.
                    //수학적 계산을 위한 중간 다리 역할을 하는 것 같습니다.
                    //Inverse()에 대한 메뉴얼도 마땅치 않습니다. 위치와 회전에 관해 뒤집어 주는 역할인 것 같습니다. 스프링에 매달린 형태를 떠올려 보면 맞는 것 같습니다.
                    MTransform bodyFromWorld = Inverse(new MTransform(hitBody.WorldFromBody));
                    //좌표를 알아내는 행위 같습니다.
                    float3 pointOnBody = Mul(bodyFromWorld, mousePickCollector.Hit.Position);

                    SpringData[0] = new SpringData
                    {
                        Entity = hitBody.Entity,
                        Dragging = 1,
                        PointOnBody = pointOnBody,
                        //카메라로부터의 거리를 의미하는 것 같습니다.
                        MouseDepth = Near + math.dot(math.normalize(RayInput.End - RayInput.Start), Forward) * fraction * k_MaxDistance,
                    };
                }
                else
                {
                    SpringData[0] = new SpringData
                    {
                        Dragging = 0
                    };
                }
            }
        }

        //생성자
        public MousePickSystem()
        {
            SpringDatas = new NativeArray<SpringData>(1, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
            SpringDatas[0] = new SpringData();
        }

        //시스템 생성시 호출
        protected override void OnCreate()
        {
            m_BuildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
            m_MouseGroup = GetEntityQuery(new EntityQueryDesc
            {
                All = new ComponentType[] { typeof(MousePick) }
            });
        }

        //시스템 파괴시 호출
        protected override void OnDestroy()
        {
            SpringDatas.Dispose();
        }


        //시스템 매 프레임 호출
        protected override void OnUpdate()
        {
            //MousePick 컴포넌트 데이터를 갖고 있는 엔티티가 없다면 계산하지 않습니다.
            if (m_MouseGroup.CalculateEntityCount() == 0)
            {
                return;
            }

            // 종속성을 결합합니다.
            var handle = JobHandle.CombineDependencies(Dependency, m_BuildPhysicsWorldSystem.GetOutputDependency());

            //마우스 왼쪽 버튼을 누르고 카메라가 있을 때
            if (Input.GetMouseButtonDown(0) && (Camera.main != null))
            {
                //기존 방식으로 Ray를 생성합니다.
                Vector2 mousePosition = Input.mousePosition;
                UnityEngine.Ray unityRay = Camera.main.ScreenPointToRay(mousePosition);

                //MousePick 배열을 가져옵니다.
                var mice = m_MouseGroup.ToComponentDataArray<MousePick>(Allocator.TempJob);
                //배열의 첫번째 것으로만 트리거를 무시할지에 대해 가져옵니다.
                var IgnoreTriggers = mice[0].IgnoreTriggers != 0;
                //배열의 메모리를 해체합니다.
                mice.Dispose();

                // Schedule picking job, after the collision world has been built
                // Pick 작업을 스케줄합니다. 충돌 세계가 생성된 후에 작업할 것 입니다.
                handle = new Pick
                {
                    CollisionWorld = m_BuildPhysicsWorldSystem.PhysicsWorld.CollisionWorld,
                    NumDynamicBodies = m_BuildPhysicsWorldSystem.PhysicsWorld.NumDynamicBodies,
                    SpringData = SpringDatas,
                    //기존 방식으로 생성된 Ray를 이용해 RaycastInput을 작성합니다.
                    RayInput = new RaycastInput
                    {
                        Start = unityRay.origin,
                        End = unityRay.origin + unityRay.direction * k_MaxDistance,
                        Filter = CollisionFilter.Default,
                    },
                    Near = Camera.main.nearClipPlane,
                    Forward = Camera.main.transform.forward,
                    IgnoreTriggers = IgnoreTriggers,
                }.Schedule(handle);

                PickJobHandle = handle;

                handle.Complete(); // TODO.ma figure out how to do this properly...we need a way to make physics sync wait for
                // any user jobs that touch the component data, maybe a JobHandle LastUserJob or something that the user has to set
                // TODO.ma 올바르게 수행하는 방법을 알아내야 합니다 ... 물리적 동기화가 구성 요소 데이터를 터치하는 사용자 작업, JobHandle LastUserJob 또는 사용자가 설정해야 할 작업을 기다릴 수있는 방법이 필요합니다

            }

            //마우스 왼쪽 버튼을 떼었을 경우 스프링 정보를 초기화합니다.
            if (Input.GetMouseButtonUp(0))
            {
                SpringDatas[0] = new SpringData();
            }

            //종속성 갱신
            Dependency = handle;
        }
    }

    // Applies any mouse spring as a change in velocity on the entity's motion component
    // 엔티티의 Motion 컴포넌트에서 속도 변화에 마우스 스프링을 적용합니다.
    [UpdateBefore(typeof(BuildPhysicsWorld))]
    public class MouseSpringSystem : SystemBase
    {
        EntityQuery m_MouseGroup;
        MousePickSystem m_PickSystem;

        protected override void OnCreate()
        {
            //월드에서 해당 시스템을 가져옵니다.
            m_PickSystem = World.GetOrCreateSystem<MousePickSystem>();
            m_MouseGroup = GetEntityQuery(new EntityQueryDesc
            {
                All = new ComponentType[] { typeof(MousePick) }
            });
        }

        protected override void OnUpdate()
        {
            if (m_MouseGroup.CalculateEntityCount() == 0)
            {
                return;
            }

            //각 정보를 불러옵니다. true 매개변수는 읽기전용임을 나타냅니다.
            ComponentDataFromEntity<Translation> Positions = GetComponentDataFromEntity<Translation>(true);
            ComponentDataFromEntity<Rotation> Rotations = GetComponentDataFromEntity<Rotation>(true);
            ComponentDataFromEntity<PhysicsVelocity> Velocities = GetComponentDataFromEntity<PhysicsVelocity>();
            ComponentDataFromEntity<PhysicsMass> Masses = GetComponentDataFromEntity<PhysicsMass>(true);

            // If there's a pick job, wait for it to finish
            // Pick 작업이 있다면 작업이 끝날 때 까지 대기합니다.
            if (m_PickSystem.PickJobHandle != null)
            {
                JobHandle.CombineDependencies(Dependency, m_PickSystem.PickJobHandle.Value).Complete();
            }

            // If there's a picked entity, drag it
            // 잡힌 엔티티가 있따면 드래그 합니다.
            MousePickSystem.SpringData springData = m_PickSystem.SpringDatas[0];
            //스프링 정보를 가져오고, 드래그 중인지 체크합니다.
            if (springData.Dragging != 0)
            {

                Entity entity = m_PickSystem.SpringDatas[0].Entity;
                //PhyiscsMass가 없다면 계산을 취소합니다.
                if (!EntityManager.HasComponent<PhysicsMass>(entity))
                {
                    return;
                }

                //해당 컴포넌트데이터와 속도를 가져옵니다.
                PhysicsMass massComponent = Masses[entity];
                PhysicsVelocity velocityComponent = Velocities[entity];

                //메뉴얼엔 아직 내용이 적혀있지 않습니다.
                if (massComponent.InverseMass == 0)
                {
                    return;
                }

                // 회전과 위치값을 따라 m트랜스폼 정보를 가져옵니다.
                var worldFromBody = new MTransform(Rotations[entity].Value, Positions[entity].Value);

                // Body to motion transform
                // PhysicsMass에서 mTransform으로 정보를 변환합니다.
                var bodyFromMotion = new MTransform(Masses[entity].InertiaOrientation, Masses[entity].CenterOfMass);
                MTransform worldFromMotion = Mul(worldFromBody, bodyFromMotion);

                // Damp the current velocity
                // 속도를 낮춥니다.
                const float gain = 0.95f;
                velocityComponent.Linear *= gain;
                velocityComponent.Angular *= gain;

                // Get the body and mouse points in world space
                // 월드 공간에서 강체와 마우스 포인트를 얻어옵니다.
                float3 pointBodyWs = Mul(worldFromBody, springData.PointOnBody);
                float3 pointSpringWs = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, springData.MouseDepth));

                // Calculate the required change in velocity
                // 속도 변화에 필요한 계산을 합니다.
                float3 pointBodyLs = Mul(Inverse(bodyFromMotion), springData.PointOnBody);
                float3 deltaVelocity;
                {
                    float3 pointDiff = pointBodyWs - pointSpringWs;
                    float3 relativeVelocityInWorld = velocityComponent.Linear + math.mul(worldFromMotion.Rotation, math.cross(velocityComponent.Angular, pointBodyLs));

                    const float elasticity = 0.1f;
                    const float damping = 0.5f;
                    deltaVelocity = -pointDiff * (elasticity / UnityEngine.Time.fixedDeltaTime) - damping * relativeVelocityInWorld;
                }

                // Build effective mass matrix in world space
                // 월드 공간에서 질량 매트릭스를 구축합니다.
                // TODO how are bodies with inf inertia and finite mass represented
                // TODO 관성 및 유한 질량을 가진 바디는 어떻게 표현해야 할까
                // TODO the aggressive damping is hiding something wrong in this code if dragging non-uniform shapes
                // TODO 균일하지 않은 모양을 드래그하면 공격적인 댐핑이 이 코드에서 잘못된 것을 숨겨버립니다.
                float3x3 effectiveMassMatrix;
                {
                    float3 arm = pointBodyWs - worldFromMotion.Translation;
                    var skew = new float3x3(
                        new float3(0.0f, arm.z, -arm.y),
                        new float3(-arm.z, 0.0f, arm.x),
                        new float3(arm.y, -arm.x, 0.0f)
                    );

                    // world space inertia = worldFromMotion * inertiaInMotionSpace * motionFromWorld
                    // 월드 공간에서의 관성 =
                    var invInertiaWs = new float3x3(
                        massComponent.InverseInertia.x * worldFromMotion.Rotation.c0,
                        massComponent.InverseInertia.y * worldFromMotion.Rotation.c1,
                        massComponent.InverseInertia.z * worldFromMotion.Rotation.c2
                    );
                    invInertiaWs = math.mul(invInertiaWs, math.transpose(worldFromMotion.Rotation));

                    float3x3 invEffMassMatrix = math.mul(math.mul(skew, invInertiaWs), skew);
                    invEffMassMatrix.c0 = new float3(massComponent.InverseMass, 0.0f, 0.0f) - invEffMassMatrix.c0;
                    invEffMassMatrix.c1 = new float3(0.0f, massComponent.InverseMass, 0.0f) - invEffMassMatrix.c1;
                    invEffMassMatrix.c2 = new float3(0.0f, 0.0f, massComponent.InverseMass) - invEffMassMatrix.c2;

                    effectiveMassMatrix = math.inverse(invEffMassMatrix);
                }

                // Calculate impulse to cause the desired change in velocity
                // 원하는 속도 변화를 일으키는 충격(impulse)를 계산합니다.
                float3 impulse = math.mul(effectiveMassMatrix, deltaVelocity);

                // Clip the impulse
                // 최대 충격값에 따라 잘라냅니다.
                const float maxAcceleration = 250.0f;
                float maxImpulse = math.rcp(massComponent.InverseMass) * UnityEngine.Time.fixedDeltaTime * maxAcceleration;
                impulse *= math.min(1.0f, math.sqrt((maxImpulse * maxImpulse) / math.lengthsq(impulse)));

                // Apply the impulse
                // 충격 값을 적용합니다.
                {
                    velocityComponent.Linear += impulse * massComponent.InverseMass;

                    float3 impulseLs = math.mul(math.transpose(worldFromMotion.Rotation), impulse);
                    float3 angularImpulseLs = math.cross(pointBodyLs, impulseLs);
                    velocityComponent.Angular += angularImpulseLs * massComponent.InverseInertia;
                }

                // Write back velocity
                // 속도값을 덮어씁니다.
                Velocities[entity] = velocityComponent;
            }
        }
    }
}

- Hierarchy/Physics Settings : 

Mouse Pick Behaviour를 제외하곤 패키지 내장 스크립트입니다.

Physics Settings의 인스펙터

 -> Physics Debug Display는 각 옵션에 따라 화면에 그리는 기즈모를 달리해줍니다.

 -> Physics Step은 물리 연산의 시뮬레이션에 영향을 끼칩니다.

 

 

- Physics Shape : 

 물리 연산에 필요한 형태와 성질을 표시하며, 충돌체(콜라이더)를 나타냅니다.

- Physics Body : 

 물리 연산에 필요한 특성을 표시하며, 강체(리지드바디)를 나타냅니다.


예제에서는 엔티티를 잡고 드래그하는 정도로만 되어있지만 파다보니 엄청 복잡하네요. 전체적인 동작 방식은 이해를 했지만 세세한 계산한 부분은 제가 수학적 지식도 짧아서 그런지 다 이해하진 못했습니다.

 

 아쉬운 점은 여전히 메뉴얼이 빈 부분이 엄청나게 많았다는 점 입니다.

반응형