Home 11. 벡터와 행렬
Post
Cancel

11. 벡터와 행렬

머릿말

… 이걸 한 주 만에 다 한다고요..?

3주차부터 13주차까지 했던 내용이 한 주에 들어갑니다.

Untitled

과거의 지식 ON

(하지만 설명의 방식때문에 사용할 일은 없었다)

Built-in shader variables

https://docs.unity3d.com/2021.2/Documentation/Manual/SL-UnityShaderVariables.html

벡터

수학 : 2주차 과제 중 일부 벡터는 화살표입니다. 어느 방향으로 얼마만큼 이라는 정보를 가진 화살표 입니다. 이 화살표에게 크기를 늘린다는것은, 진행하는 방향을 하나로 축으로 하는 직선에서의 크기를 늘리는 것입니다.

즉 벡터의 공간은 1차원 직선입니다.

이런 벡터는 정말 많은 곳에 사용됩니다.

위치를 표기하거나, 이동하거나, 유사도를 구하거나 합니다.

벡터는 각 성분별로 구성되어 있는데,

실수 N 집합의 원소들로 만들어지는 순서쌍 이라고 생각하면 되는 것이죠.

벡터의 덧셈과 뺄셈

벡터와 벡터의 덧셈은, 각 순서(order)에 맞는 스칼라끼리 덧셈을 한 것과 같습니다.

따라서 (a,b)+(c,d)=(a+c,b+d)(a,b) + (c,d) = (a+c,b+d) 입니다.

덧셈의 개념에서 합성, 즉 이동을 알 수 있습니다.

뺄셈은 덧셈의 역원을 더하는 것과 같기 때문에

(a,b)(c,d)=(a+(c),b+(d))(a,b)(c,d)=(ac,bd)(a,b) - (c,d) = (a+(-c),b+(-d)) \\ \therefore (a,b) - (c,d) = (a-c,b-d)

해주면 됩니다.

뺄셈의 경우에는 변위, 즉 차이를 알 수 있기 때문에

특정 에서 특정 을 뺀 ‘방향’ 을 알 수 있습니다.

벡터의 스칼라곱

다시 돌아와서, 스칼라배는 스칼라값을 모든 순서의 벡터내 스칼라에 곱하면 됩니다.

따라서 (1,2) α=(1 α,2 α)(1,2)\space \alpha = (1 \space \alpha,2\space \alpha) 입니다

해당연산을 통해

  • 벡터의 방향을 반대로 하거나(수직선에서 방향을 반대로 한 것과 같이)
  • 벡터의 길이를 변화시킬 수 있습니다.

그저 적용되는게 수직선에서 벡터가 가르키는 방향으로 바뀔 뿐입니다.

벡터의 정규화

벡터의 크기를 1로 만들기 위한 연산입니다.

방향만을 가리키는 것을 원할때 구하게 됩니다.

구하는 것은 각 성분의 제곱의 크기의 합으로 각 성분을 나누어줍니다.

삼각형의 빗변이 1이 되기 위해 각 성분을 삼각형의 빗변으로 나누는 행동과 같습니다.

코드로 표현하면 다음과 같습니다.

1
2
3
4
5
6
void Normalize(this Vector2 vector)
{
	  var norm = sqrt(vector.x * vector.x + vector.y * vector.y)
	  vector.x /= norm;
	  vector.y /= norm;
}

벡터의 내적

내적은 Cos, 투영입니다.

교환법칙이 성립하며

결합법칙은 성립하지 않습니다.

분배법칙은 성립합니다

두 벡터의 내적은 각 벡터의 크기의 곱에 cos𝜽한 값과 같은데,

자주 사용하는 내적들은 보통 정규화 해서 사용하므로 cos𝜽만 생각하면 됩니다.

이를 응용하면 어떤 케이스가 있을까요? 보통 ‘각도’에 대한 값으로 생각하면 좋습니다.

시야각 내의 물체판별을 예로 들어보겠습니다.

https://ashuatz.github.io/assets/(Graphic)11/Untitled%201.png

시야각 내의 물체 판별

  1. 시야각의 경우, θ\theta degree라고 가정할 때, cos(θ2)cos(\frac{\theta}{2}) 값을 미리 구워둡니다.
  2. 그리고 물체의 위치(점) 에서 시야의 위치(점)을 뺀 벡터를 구합니다.
  3. 시야의 정면 벡터와 해당 벡터를 내적한 값을 구합니다.
  4. 구워둔 시야각의 값과 3.에서 내적한 벡터의 값의 차이를 통해 내부와 외부를 판별합니다.

예를 들어 시야각 60도 일 경우, 30에 해당하는 0.866이 구워진 값이며,

시야정면벡터와, 오브젝트와 시야의 변위벡터의 내적을 통해 0.866 보다 크다면 시야에 포함,

0.866보다 작다면 시야에 포함되지 않음 입니다.

시야각 뿐만이 아닌 빛과 표면의 각도, 백어택,

뭐 그런 ‘각도’에 대한 것을 곱셈과 덧셈만으로 가볍게 구할 수 있어 자주사용합니다.

벡터의 외적

외적은 임의 두 벡터로부터 또다른 벡터량을 생성해내는 연산입니다.

계산하고나면 남는건 두 벡터의 크기의 곱에 sin𝜽에 두 벡터의 수직 방향을 곱한 꼴이 됩니다.

Untitled

기본적으로 교환법칙이 비성립하며,

결합법칙도 비성립, 분배법칙만 성립합니다.

여기서 교환법칙을 다시 풀어보면

Untitled

순서가 반대로 된 것을 볼 수 있는데,

Untitled

그에 따라 위 두식이 일치한다고 볼 수 있습니다.

여기서 반수성질 을 알 수 있습니다.

Untitled

외적은 활용할 때 좌우판별, 앞뒤판별을 할 수 있습니다.

좌우판별

바라보는 방향과 물체의 방향을 외적한 값은 upVector와 평행한 벡터가 나오게 되는데,

이를 설정하여 내적하여 유사도를 구한 다음, 그 값의 부호로 판별하는 방법입니다.

카메라의 업벡터를 통해 좌(subtractive)와 우(additive) 의 색상이 변경됩니다.

GIF.gif

후면컬링

후면컬링, 백페이스 컬링은 단순합니다.

좌표계에 따라 인덱스버퍼에 정점 인덱스를 감기 시작한다면

Direct3D9, Left Handed Direct3D9, Left Handed

Untitled

다음과 같이 됩니다.

이 세점을 통해 두 벡터를 구하고, 두 벡터의 외적을 통해 면의 노멀을 구합니다.

면의 노멀을 구했으면 어느방향으로 나가는지 알기때문에, 뒷면을 모두 안그릴 수 있는 것입니다.

shader graph로 대충 테스트해봅시다.

Two Side (양면렌더링) Lit 쉐이더를 하나 만들어 줍니다.

양면을 보여줘야 하니까 Transparent, 양면을 보여줘야 하니까 TwoSide.

Untitled

그럼 이런게 나옵니다.

Untitled

그러면 이제 위에서 설명한대로 만들어 볼까요?

Untitled

카메라에서 해당 면(여기서는 픽셀입니다)의 방향(Position - CameraPosition)과 노멀(Normal)을 내적합니다.

그리고 그 값이 0보다 작은 값들, 즉 마주보는 방향에 따라

Untitled

마주본다면 1, 그렇지 않다면 0의 값을 투명도에 곱합니다.

그러면 짠.

Untitled 라이트 색상 좀 잘 받는 각도로 카메라를 옮겼습니다. 왜 색상이 기즈모를 안따르냐고요? 제 맘입니다.

Untitled

지금은 시각적으로 보이지 않게 하기 위해 투명도를 0으로 설정하였지만,

실제 연산은 비교(Comparison)를 통해 얻은 값에 따라 렌더링 할지에 대한 여부를 고르면 됩니다.

위: URP lit Transparent alpha twoside 중간 : URP lit Transparent alpha front 아래 : 구현물

위: URP lit Transparent alpha twoside 중간 : URP lit Transparent alpha front 아래 : 구현물

구현된 컬링처럼 보이는 그것 구현된 컬링처럼 보이는 그것

표준기저벡터

기저벡터를 이해하기 가장 좋은 유투브 자료 일단 드셔보십쇼! 이게 답입니다!

기저와 기저벡터

임의의 벡터들이 선형의존관계가 아니라면 생성이 가능합니다

이 생성이 가능하게 만드는 벡터들을 기저벡터(basis vector) 라고 합니다.

그리고 이 선형독립인 벡터들의 집합을 기저(basis)라고 합니다.

표준기저벡터

그리고 표준기저벡터(Standard Basis Vector) 라는 존재가 있습니다.

아까 위에서 설명하였던 단위벡터의 기저벡터 버전이라고 보시면 됩니다.

계산을 편리하게 만들고, 가장 개념적으로 타이트하기 쉬운 벡터이며,

성분 1개만이 1이며 나머지 성분이 모두 0인 벡터입니다.

예를 들어 데카르트 좌표계에서는 (1,0) , (0,1) 의 두 개의 벡터가 표준 기저벡터입니다.

이 표준 기저 벡터를 통해 좌표에 존재하는 모든 지점을 각각의 성분에 스칼라배를 한 값으로 나타낼 수 있습니다.

행렬

행렬이란 무엇인가?

수를 1차원적으로 나열한 체계를 벡터라고 하였다면, 수, 문자, 함수등을 네모꼴 괄호 안에 배열하여 2차원 놓은 것이 행렬입니다.

선형변환(Linear Transformation)를 좀 더 편하게 하기 위하여 단순화 시킨 도구라고합니다.

행(row)과 열(column)을 가지고 있으며 다음과 같이 표기합니다.

Untitled

또한 행이 하나이거나, 열이 하나인 경우를 볼 수 있는데, 모습이 마치 벡터와 같습니다.

그리고 이걸 수학적으로 행이 하나인 행렬은 행벡터(row vector)

Untitled

열이 하나인 행렬은 열벡터(column vector)

Untitled

라고 합니다.

행렬의 덧셈,뺄셈 그리고 스칼라곱

덧셈과 뺄셈은 단순히 똑같은 위치에 똑같은 값을 ‘연산’합니다.

하지만 곱셈은 조금 다릅니다.

행렬의 곱셈은 선형변환에 사용할 수 있게 해주는 수학적 도구라고 하였는데,

정확히 어떻게 동작하는지 본다면 그 의미를 알 수 있을 것 같습니다.

일단 기본적으로 스칼라와 행렬의 곱은 단순하게도 모든 원소에 대해 스칼라배 해주는 것과 같습니다. 마치 벡터에서의 스칼라배와 마찬가지로 말이죠.

A=[abcd]kA=[kakbkckd]\bold A = \begin{bmatrix} a & b\\ c & d \end{bmatrix} \\ k\bold A = \begin{bmatrix} ka & kb\\ kc & kd \end{bmatrix}

그리고 메인인 행렬과 행렬의 곱셈을 알아봅시다.

간단하게 행렬의 곱은, 행과 열끼리 곱연산한 결과의 합을 저장합니다.

행렬 A\bold AB\bold B 의 곱은 AB\bold{AB} 로 쓰고, 다음과 같이 정의합니다.

AB(i,j)\bold{AB}(i,j) 는, A\bold Aii 번째 행이 이루는 행벡터와 B\bold Bjj 번째 열이 이루는 열벡터의 내적이다”

무슨소리냐고요?

https://ashuatz.github.io/assets/(Graphic)11/assignment_GIF_5.gif

이것은 처리를 시각적으로 보여준 것입니다.

기저벡터를 통한 행렬, 공간구성

아까 설명했던 기저벡터를 늘어놓게 된다면 하나의 행렬이 구성되는데,

기존 크기 * 기저A + 크기 * 기저B 의 형태에서 기저만 변형된 것이기 때문에

이를 기저벡터로 구성된 하나의 공간이라고 보아도 괜찮습니다.

트랜스폼과 공간변형

그전에 각 공간에 대해 알아봅시다.

오브젝트 좌표계 (Object space, 로컬좌표계 혹은 모델좌표계)

각 오브젝트의 중심이 되는 좌표계입니다.

월드 좌표계 (World Space)

월드좌표계는 Scene의 중심점을 중심으로하여 Scene안에서 여러개의 오브젝트가 공간적으로 어느정도 관계가 있는가를 표시하는 좌표계입니다.

오브젝트의 이동 회전 크기를 다루는 ModelingTransform에 의해 오브젝트 공간으로부터 변환됩니다.

뷰 좌표계(View Space, 시점 혹은 카메라 좌표계)

뷰 좌표계는 렌더링하는 카메라를 중심으로 그 시점을 중심점으로 설정한 좌표계입니다.

카메라의 위치, 카메라의 업벡터, 카메라의 바라보는 방향 등의 정보를 정의한 뷰 행렬이 존재하는데,

이를 이용하여 월드 좌표계에서 View Transform에 의해 View Space로 변환될 수 있습니다.

클립 좌표계(Clip Space)

클립 좌표계는 위에서 사용한 ViewMatrix에 정의한 카메라 외의 정보를 추가한

Projection Matrix를 뷰 좌표계에서 계산하여 얻어지는 좌표계입니다.

Projection Matrix는 FOV, 종횡비, near clip, far clip등의 정보를 추가로 정의하며,

이 행렬을 적용하는 계산을 Projection Transform이라 합니다.

최종적으로 카메라의 렌더링 공간을 클리핑합니다.

동차 좌표계 (Homogeneous Coordinates)

3차원의 위치값은 (x,y,z) 이지만 가끔 (x,y,z,w)의 형태처럼

4차원으로 취급해야할 때가 있습니다 (아핀공간 처럼요)

이렇게 나타낸 좌표를 동차 좌표라고 부릅니다(1차,2차 방정식처럼 차가 같다는 뜻입니다).

이렇게 위치를 4차원으로 변환하는것으로 4x4 행렬에 효과적으로 계산하는것이 가능해집니다.

좌표변환의 계산은 기본적으로 4x4행렬로 계산하므로, 위치벡터는 이처럼 4차원으로 표현합니다 (아핀공간의 점의 이동을 생각해본다면 차원보다 1단계 높은 행렬이 필요합니다)

동차와 비동차의 변환은 (x/w, y/w, z/w, 1) = (x,y,z,w) 의 식과 같이 이루어 집니다.

NDC 좌표계(Normalized Device Coordinates)

Clip 좌표계에 의해 계산된 좌표의 성분에 w를 나누는 것으로

-1<성분<1 의 값으로 모든 성분을 정규화합니다.

이 계산으로 구할 수 있는 좌표계를 NDC 좌표계라고 합니다.

이 변환은 Perspective divide라고 불리며 말에서 알아챌 수 있듯 원근에 따라 앞의 오브젝트는 크게, 멀리있는 오브젝트는 작게 표현됩니다.

스크린 좌표계 (Screen Space)

NDC 좌표계의 수치를 화면 해상도에 맞게 변환한 좌표계입니다.

실습 - 포스트프로세싱을 통한 색상변경


이번엔 포스트프로세싱을 건드려볼 생각입니다.

그것도 렌더 파이프라인에 렌더피처를 넣어서요.

Untitled

최종적으론 위과 같이 나타납니다.

렌더 피처에서 프레임 버퍼를 받아와 렌더 텍스처에 한번 작성한 다음,

해당 텍스처를 이번엔 머테리얼을 통해 프레임버퍼에 다시 렌더링합니다.

이 머테리얼은 vertex fragment 쉐이더에 mainTexture 하나가 존재합니다.

그리고 이 머테리얼은 세가지 기저 색상을 나타내며,

Untitled

Untitled

색상을 변경할 경우 최종적으로 화면에 그려지는 색상은

‘기저 세개로 구성된 행렬의 전치행렬(선언시 그냥 바로 넣었다는 뜻)’을 곱하여,

새로운 기저에 맞는 색상으로 처리합니다.

'기저벡터' 로써 사용하자면 transpose한게 맞는 의미입니다.하지면 이번엔 그냥 사용합니다. ‘기저벡터’ 로써 사용하자면 transpose한게 맞는 의미입니다.하지면 이번엔 그냥 사용합니다.

Untitled

Untitled

Untitled

Untitled

Untitled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class FinalRenderFeature : ScriptableRendererFeature
{

    [System.Serializable]
    public class FinalSettings
    {
        public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
        public Material passMaterial = null;
    }

    public FinalSettings settings = new FinalSettings();

    class FinalRenderPass : ScriptableRenderPass
    {

        public Material passMaterial;
        string profilerTag;

        int tmpId1;

        RenderTargetIdentifier tmpRT1;
        RenderTargetIdentifier cameraColorTexture;

        public FinalRenderPass(string profilerTag)
        {
            this.profilerTag = profilerTag;
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            tmpId1 = Shader.PropertyToID("tmpBlurRT1");

            cmd.GetTemporaryRT(tmpId1, cameraTextureDescriptor.width, cameraTextureDescriptor.height, 0, FilterMode.Bilinear, RenderTextureFormat.ARGB32);

            tmpRT1 = new RenderTargetIdentifier(tmpId1);

            ConfigureTarget(tmpRT1);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            cameraColorTexture = renderingData.cameraData.renderer.cameraColorTarget;
            CommandBuffer cmd = CommandBufferPool.Get(profilerTag);

            RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
            opaqueDesc.depthBufferBits = 0;

            cmd.Blit(cameraColorTexture, tmpRT1);
            cmd.Blit(tmpRT1, cameraColorTexture, passMaterial);

            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            CommandBufferPool.Release(cmd);
        }

        public override void FrameCleanup(CommandBuffer cmd)
        {

        }
    }

    // Unity RenderPipeline

    private FinalRenderPass scriptablePass;

    public override void Create()
    {
        scriptablePass = new FinalRenderPass("Final");
        scriptablePass.passMaterial = settings.passMaterial;
        scriptablePass.renderPassEvent = settings.renderPassEvent;
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(scriptablePass);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
Shader "Hidden/FinalShader"
{
    Properties
    { 
        [HideInInspector]_MainTex ("Texture", 2D) = "white" {}
        _Axis_R ("Axis R", Color) = (1,0,0,1)
        _Axis_G ("Axis G", Color) = (0,1,0,1)
        _Axis_B ("Axis B", Color) = (0,0,1,1)
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" }

        Cull Off ZWrite Off ZTest Always
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"            

            struct Attributes
            {
                float4 positionOS   : POSITION;      
                float2 uv : TEXCOORD0;           
            };

            struct Varyings
            {
                float2 uv : TEXCOORD0;
                float4 positionHCS  : SV_POSITION;
            };            

            CBUFFER_START(UnityPerMaterial)
                sampler2D _MainTex;
                float4 _Axis_R;
                float4 _Axis_G;
                float4 _Axis_B;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = IN.uv;
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                float4 col = tex2D(_MainTex, IN.uv);

                float3x3 colorMatrix = float3x3(
                    _Axis_R.rgb,
                    _Axis_G.rgb,
                    _Axis_B.rgb
                );

                col.rgb = mul(colorMatrix,col.rgb);
                return col;
            }
            ENDHLSL
        }
    }
}

최종적으로 뽑아본 화면은 다음과 같습니다.

image1

image2

실습(?) - 풀 쉐이더 상호작용

(이라쓰고 그냥 하고 싶은거 한 것 자랑)

Untitled

사실 저번주에 작성한 풀 쉐이더에서,

모든 공간변환이 이번주차에 배운 내용이기 때문에 좀 다시읽으면 흥미로울 수 있습니다.

아무튼 저번주 과제물을 완성한 후, 잠을 자다가 그런생각을 했습니다.

어? 마스크맵이 있으면, 그냥 텍스처하나 그려서 풀을 지우거나, 눌리거나 할 수 있는거 아닌가?

그렇게 머릿속으로 곰곰히 잘 숙성하다가 개발을 진행해 보았습니다.

골자는 다음과 같습니다.

만약 공격이 발생한다면, 해당지점에 raycast를 사용하여 uv좌표를 가져옵니다.

맵에 사용될 렌더텍스처의 uv좌표에 색을 칠합니다.

1. 상호작용할 위치 가져오기

Untitled

간단하게 오브젝트가 위치한 지점의 uv를 가져옵니다.

해당uv에 적절하게 색을 칠해줘야하는데, uv는 [0~1]의 값을 가지고,

픽셀은 화소, 즉 정수 단위(1,2,3..)의 구성이므로

텍스처 사이즈와 메쉬의 바운드와 트랜스폼의 크기를 가져와서 적당히 잘 섞어줍니다.

2. 칠하기

지금 가져온건 하나의 픽셀이므로, 대략 너비를 지정해서 그 영역을 다 칠하게 합니다.

원을 그리는 방법으로 해도되고 뭘로해도 되는데, 저는 예제가 큐브이기 때문에 사각형으로 칠합니다.

Untitled

색을 칠할때는 고려해야할 사항이 조금 있습니다.

칠할때의 위치가 만약 0이나 텍스처의 크기가 넘어가면, 반대쪽 부분에 그려진다는 점인데요,

약간 유사-repeat 같은느낌의 오류가 발생합니다.

그러니 Clamp 처리를해서 삐져나가지 않도록 합니다.

그러면 텍스쳐가 그려지고, 몇몇 지점에 대해 색상을 칠했습니다.

Untitled

3. 쉐이더 처리

이제 쉐이더에서 처리해줍니다.

기존맵을 가져와서 보이는영역은 r채널의 값을,

굽힘처리될 영역은 g채널의 값을 가져오도록 합니다.

그리고 굽힘 처리된 영역은 바람을 받지 않도록 windSample에서 한번 곱해주고요.

Untitled

짜잔

이 예제는 clamping 처리하지 않아 처음 이동할때 반대쪽(좌상단)의 풀이 사라짐을 알 수 있습니다. 이 예제는 clamping 처리하지 않아 처음 이동할때 반대쪽(좌상단)의 풀이 사라짐을 알 수 있습니다.

GIF 2022-05-23 17-18-57.gif

대충 예제는 이렇습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using System.Linq;

public class GrassTextureInteraction : MonoBehaviour
{
    [SerializeField]
    private List<Transform> HitTarget;

    [SerializeField]
    private List<Transform> BendTargets;

    [SerializeField]
    private List<Transform> HeightSettingTargets;

    [SerializeField]
    private float reloadRuntime = 0.5f;

    [SerializeField]
    private float heightOverrideValue;

    [SerializeField]
    private float radius;

    [SerializeField]
    private MeshFilter grassGroundMeshFilter;

    [SerializeField]
    private RenderTexture renderTexture;

    private Texture2D texture;

    private readonly List<Vector2> HitPoints = new List<Vector2>();
    private readonly List<Vector2> BendPoints = new List<Vector2>();
    private readonly List<Vector2> HeightSettings = new List<Vector2>();

    void Awake()
    {
        texture = new Texture2D(renderTexture.width, renderTexture.height);
    }

    private void OnEnable()
    {
        //ClearTexture();
        StartCoroutine(UpdateLerping());
    }

    IEnumerator UpdateLerping()
    {
        float t = 0;
        var defaultColors = texture.GetPixels32();
        while (t < reloadRuntime)
        {
            LerpTexture(defaultColors, t / reloadRuntime);
            t += Time.deltaTime;
            yield return null;
        }
    }

    private void FixedUpdate()
    {
        HitPoints.Clear();
        BendPoints.Clear();
        HeightSettings.Clear();
        
        foreach (var target in HitTarget)
        {
            var ray = new Ray(target.position, Vector3.down);

            if (Physics.Raycast(ray, out var hit, 1))
            {
                HitPoints.Add(hit.textureCoord);
            }
        }

        foreach(var target in BendTargets)
        {
            var ray = new Ray(target.position, Vector3.down);

            if (Physics.Raycast(ray, out var hit, 1))
            {
                BendPoints.Add(hit.textureCoord);
            }
        }

        foreach (var target in HeightSettingTargets)
        {
            var ray = new Ray(target.position, Vector3.down);

            if (Physics.Raycast(ray, out var hit, 1))
            {
                HeightSettings.Add(hit.textureCoord);
            }
        }

        ApplyToRenderTexture();
    }

    private void LerpTexture(in Color32[] colors, float t)
    {
        var targetColor = Color.white;
        var list = new List<Color32>();

        foreach (var color in colors)
        {
            list.Add(Color.Lerp(color, Color.white, t));
        }

        texture.SetPixels32(list.ToArray());
        texture.Apply();
    }

    private void ClearTexture()
    {
        Color32 fillColor = Color.white;
        var colors = Enumerable.Repeat(fillColor, texture.width * texture.height).ToArray();

        texture.SetPixels32(colors);
        texture.Apply();
    }

    private void ApplyToRenderTexture()
    {
        var bound = grassGroundMeshFilter.mesh.bounds.size;
        Vector2Int factor = new Vector2Int(
            Mathf.RoundToInt(radius * texture.width / (bound.x * grassGroundMeshFilter.transform.lossyScale.x)),
            Mathf.RoundToInt(radius * texture.height / (bound.z * grassGroundMeshFilter.transform.lossyScale.z)));

        PaintColor(factor, HitPoints, Color.red, 0);
        PaintColor(factor, BendPoints, Color.green, 0);
        PaintColor(factor, HeightSettings, Color.blue, heightOverrideValue);

        texture.Apply();

        Graphics.Blit(texture, renderTexture);
    }

    void PaintColor(in Vector2Int factor, in List<Vector2> points, in Color mask, in float value)
    {
        foreach (var point in points)
        {
            var pos = new Vector2Int(Mathf.FloorToInt(point.x * texture.width), Mathf.FloorToInt(point.y * texture.height));

            var xMin = Mathf.Clamp(pos.x - factor.x, 0, texture.width);
            var xMax = Mathf.Clamp(pos.x + factor.x, 0, texture.width);
            var yMin = Mathf.Clamp(pos.y - factor.y, 0, texture.height);
            var yMax = Mathf.Clamp(pos.y + factor.y, 0, texture.height);

            for (int x = xMin; x < xMax; ++x)
            {
                for (int y = yMin; y < yMax; ++y)
                {
                    var color = texture.GetPixel(x, y);

                    color -= color * mask;
                    color += mask * value;

                    texture.SetPixel(x, y, color);
                }
            }
        }
    }
}
This post is licensed under CC BY 4.0 by the author.
Contents