[DevLog] 안녕하세요 떵-개 오늘의 먹방은... 픽셀컴포져 짭 먹어볼께요!!😇🪽

profile image 02BBQ 2025. 10. 21. 16:32

네 안녕하세요 떵-개에요 오늘의 먹빵은....

픽셀 컴포져2 (SigmaComposer)(가제)


개요:

 아 취업!!! 해서 로블록스하면서 담배 ㅈㄴ펴야지!!!! 그러면서 잼민이들 괴롭히기 ㅎㅎ!!! (갑자기 생긴 꿈)

-> 그럼 포폴이 있어야 되네? ㅠㅠㅠㅠㅠ 도스레바이이 ㅠㅠ

-> 어 잠시만 잠시만 좆도 마때.... 출시경험이 중요하다고 했지!!! 그리고 나는 모바일 출시경험 X

-> 아 근데 나는 게임 만들기 싫은데... (게임 개발잔데 1년동안 게임만들다 다 접고 게임도 접고 망한사람)

     -> 진심 게임 만들때 마다 망함... 혼자 만들기 힘드노 ㅠㅠ

게임제작 고자

-> 그럼 내가 좋아하고 사람들도 마이너하게 관심 가질걸 해보자!!!

-> Shader Graph를 폰으로 할 수 있다면 사람들이 무언가를 만들지 않을까?? 하하하하하 난 천재여!!!

-> PixelComposer x Unity ShaderGraph x AfterEffect Let's GO!!!!!!

무야징

개꿀잼 플젝, 바로 무야지 ㅎㅎ

하ㅏ하하하하하 우리가 오늘 볼 결론을 스포해주마!!!!!

여기까지 구현됩니다 ㅎㅎ.

약간 연결선 위치가 이상하게 벗어나긴 하는데


그럼 이제 구현한거 보러 레쓰 고우!!! 개발 과정까진 장담 못하고

그리고 AI를 많이 사용했기 때문에 어 설명을 아주 풀어쓸꺼임 나도 좀 읽어야겠어 음.

머리 말

중간중간 긴코드는 안 읽어도 됨. 근데 내부가 궁금하면 보세용.

제 0장 존나 기획 바탕 기반 배경 등등 아키텍쳐

고려해야하는 고려청자

아 형님들 이거 고려해야하는게 있어요!!! 걍 ui밖에 없습니다!!! 

카메라 없애고

UI Toolkit으로 구현할께요!! 사유: 자유로움 + Fast!!!!

어 아닌가 ㅅㅂ

그래서 툴킷으로 UI Window부터 가져올께요!!! 생성해야겠죠!!

얘가 해줍니다:

이게 최후의 게임 오브젝트임 참고로

자 일단 카메라는 뭐 없으면 그 에디터 화면에서 막 카메라없다고 쌩 난리쳐서 걍 둠.

자 이런식으로 작동합니다. Claude가 그려줬습니다. 내가 그리기 싫어!!

┌─────────────────────────────────────────────────────────────┐
│                     MAIN LOOP                               │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
        ┌───────────────────────────────────┐
        │   [1] 사용자 입력 대기             │
        │   - 노드 추가/삭제                │
        │   - 연결 생성/제거                │
        │   - 파라미터 조정                 │
        │   - Execute 버튼                  │
        └───────────────────────────────────┘
                            │
                            ▼
        ┌───────────────────────────────────┐
        │   [2] Command 실행                │
        │   - AddNodeCommand                │
        │   - ConnectSlotsCommand           │
        │   - DeleteNodeCommand             │
        └───────────────────────────────────┘
                            │
                            ▼
        ┌───────────────────────────────────┐
        │   [3] NodeGraph 업데이트          │
        │   - nodes[] 변경                  │
        │   - connections[] 변경            │
        └───────────────────────────────────┘
                            │
                            ▼
        ┌───────────────────────────────────┐
        │   [4] 그래프 실행                 │
        │   - Topological Sort              │
        │   - 순서대로 Execute()            │
        └───────────────────────────────────┘
                            │
                            ▼
        ┌───────────────────────────────────┐
        │   [5] GPU 연산                    │
        │   - Shader 실행                   │
        │   - Texture 생성                  │
        └───────────────────────────────────┘
                            │
                            ▼
        ┌───────────────────────────────────┐
        │   [6] 결과 캐싱                   │
        │   - cachedOutputs 저장            │
        └───────────────────────────────────┘
                            │
                            ▼
        ┌───────────────────────────────────┐
        │   [7] UI 업데이트                 │
        │   - Preview 갱신                  │
        │   - Connection 다시 그리기        │
        └───────────────────────────────────┘
                            │
                            ▼
                    (다시 1번으로)

제 1장 배경 구현하기!!

Graph 특유의 배경을 구현해 볼께요 ㅎㅎ!!!

격자

자 이제 걍 격자 도배하면 됩니다. 근데 이거 막 임계점 넘어갈때마다 촘촘해져야해요.

무한 느낌으로 다가 줌을 엄청하면 그 안에 격자들이 나타나고 이런식으로 계속 그려야합니다!!!

일단 위에있는 ComposerWindow.cs 에서 이거 얘 ui 만들어줄께요!!

uiDocument = gameObject.AddComponent<UIDocument>();

if (panelSettings != null)
{
    uiDocument.panelSettings = panelSettings;
}

var rootVisualElement = uiDocument.rootVisualElement;
rootVisualElement.style.width = Length.Percent(100);
rootVisualElement.style.height = Length.Percent(100);

if (styleSheet != null)
{
    rootVisualElement.styleSheets.Add(styleSheet);
}

if (inspectorStyleSheet != null)
{
    rootVisualElement.styleSheets.Add(inspectorStyleSheet);
}

var horizontalContainer = new VisualElement();
horizontalContainer.style.flexDirection = FlexDirection.Row;
horizontalContainer.style.flexGrow = 1;
rootVisualElement.Add(horizontalContainer);

graphView = new NodeGraphView();
graphView.style.flexGrow = 1;
horizontalContainer.Add(graphView);

var inspector = new NodeInspector(graphView);
horizontalContainer.Add(inspector);

graphView.SetInspector(inspector);

걍 뭐 스크린 크기의 판때기를 만들고 USS들을 불러옵니다. uss는 걍 css랑 뭐 비슷합니다. 따라 만든 듯 ㅎㅎ

참고로 저는 디자인의 감각이 없어서 uss는 90% AI가 작성하였습니다. 후후 AI야 고마워!!

 

그다음에 horizontalContainer 가져옵니다. 이건 그래프 뷰(격자 뷰)랑 인스펙터 분리하려한건데 나중에 인스펙터를 팝업 윈도우 처럼 바꾸면 뭐 없어질 것임.

아무튼 GraphView와 Inspector을 Horizontal Container에 넣어줍니다. 앙 ㄱㅣ무띠!!

 

NodeGraphView라는게 있는데 이게 그 유명한 배경이오.

NodeGraphView 생성자에서 Grid그려주는 청년을 불러옵시다.

gridBackground = new GridBackground();
Add(gridBackground);

Add는 VisualElement의 함수인데 자식으로 다른 VisualElement를 추가해 줌.

public void SetOffset(Vector2 newOffset)
{
    offset = newOffset;
    MarkDirtyRepaint();
}

public void SetZoom(float newZoom)
{
    zoom = newZoom;
    UpdateGridSpacing();
    MarkDirtyRepaint();
}

private void UpdateGridSpacing()
{
    if (zoom >= 1f)
    {
        currentGridSpacing = baseGridSpacing * zoom;
        currentThickSpacing = baseThickLineSpacing * zoom;
    }
    ... (대충 중간 수치들)
    else
    {
        currentGridSpacing = baseThickLineSpacing * 5f * zoom;
        currentThickSpacing = baseThickLineSpacing * 10f * zoom;
    }

    currentGridSpacing = Mathf.Max(currentGridSpacing, 5f);
    currentThickSpacing = Mathf.Max(currentThickSpacing, 20f);
}

 

그리드 업뎃해줍니다. 근데 하드코딩임. 줌되면 더 촘촘한 그리드 나오고 그거 걍 대충 구현해서 그럼. 뭐 그래프 내부에서 이동하거나 (Offset) 줌하면 다시 그려야해요. Dirty하면 그려집니다.

 

void OnGenerateVisualContent(MeshGenerationContext ctx)
{
    var painter = ctx.painter2D;

    float width = contentRect.width;
    float height = contentRect.height;

    if (currentGridSpacing < 10f)
    {
        return;
    }

    painter.lineWidth = 1.0f;
    painter.strokeColor = lineColor;

    float startX = offset.x % currentGridSpacing;
    float startY = offset.y % currentGridSpacing;

    for (float x = startX; x < width; x += currentGridSpacing)
    {
        painter.BeginPath();
        painter.MoveTo(new Vector2(x, 0));
        painter.LineTo(new Vector2(x, height));
        painter.Stroke();
    }

    for (float y = startY; y < height; y += currentGridSpacing)
    {
        painter.BeginPath();
        painter.MoveTo(new Vector2(0, y));
        painter.LineTo(new Vector2(width, y));
        painter.Stroke();
    }

    if (currentThickSpacing >= currentGridSpacing * 2f)
    {
        float thickStartX = offset.x % currentThickSpacing;
        float thickStartY = offset.y % currentThickSpacing;

        painter.strokeColor = thickLineColor;
        painter.lineWidth = 2.0f;

        for (float x = thickStartX; x < width; x += currentThickSpacing)
        {
            painter.BeginPath();
            painter.MoveTo(new Vector2(x, 0));
            painter.LineTo(new Vector2(x, height));
            painter.Stroke();
        }

        for (float y = thickStartY; y < height; y += currentThickSpacing)
        {
            painter.BeginPath();
            painter.MoveTo(new Vector2(0, y));
            painter.LineTo(new Vector2(width, y));
            painter.Stroke();
        }
    }
}

걍 포문으로 두꺼운 줄과 얇은 줄로 그려줍니다.

이렇게 그려집니다. 바우와우

격자 배경은 끝났습니다.

 

제 2장 노드 그래프 렌더링!!

노드 그래프

노드들이나 뭐 커넥션 등등 그래프위에 많은게 그려집니다.

NodeGraphView는 노드 에디터 걍 총괄합니다. 그래프 위를 다 관리함(비주얼적으로).

GameManager급 형님입니다. 직업:

  • 모든 노드들의 시각적 표현 관리
  • 사용자 입력 처리 (클릭, 드래그, 줌, 팬)
  • 노드 간 연결선 그리기
  • Undo/Redo 시스템 관리
  • Command 패턴으로 그래프 수정

ComposerWindow 바로 아래있는 고위 간부임.

NodeGraphView (VisualElement)
│
├─ [Layer 1] GridBackground ────────── 그리드 배경
│
├─ [Layer 2] ConnectionLayer ─────── 연결선 (중간)
│
├─ [Layer 3] NodeContainer ──────── 노드들 (최상위)
│   │
│   └─ NodeView[]
│       ├─ NodeView (Noise)
│       ├─ NodeView (Multiply)
│       └─ NodeView (Output)
│
├─ [Overlay] NodeCreationMenu ────── 노드 생성 메뉴
│
└─ [References]
    ├─ NodeGraph graph ────────── Core 데이터 모델
    ├─ Dictionary<Node, NodeView> ─ Node ↔ UI 매핑
    ├─ ConnectionDragHandler ───── 연결 드래그 관리
    ├─ NodeInspector ──────────── 속성 패널
    └─ CommandHistory ─────────── Undo/Redo 스택

코드는 안까볼께요 너무 큼.

앞으로 요소들 렌더할 때 의문점이 생길 수 있기에 먼저 설명합니다.

얘가 그래프 위는 거의 다 총괄합니다.

제 3장 노드 구현하기!!

이거 걍 뭐 노드입니다. ㄹㅆㄱ

 

설명할거:

  • 노드 생성 삭제
  • 슬롯이 뭐고

노드 구현 갑시다

각 노드들은 Node를 상속받아요 아니면 뭐 망하고...

추상클래스 노드를 구현해야함!!!

public abstract class Node
{
    public string id;                    // 고유 식별자 (GUID)
    public string nodeName;              // 노드 이름
    public Vector2 position;             // UI 상 위치
    public List<NodeSlot> inputSlots;    // 입력 슬롯 리스트
    public List<NodeSlot> outputSlots;   // 출력 슬롯 리스트
    public bool isExecuted;              // 실행 완료 플래그
    public Dictionary<string, object> cachedOutputs; // 출력 캐시
}

 

 

 

이건 노드 생성자여

public Node()
{
    id = System.Guid.NewGuid().ToString();  // 고유 ID 생성
    nodeName = GetType().Name;               // 클래스명을 노드명으로
    InitializeSlots();                       // 슬롯 초기화 (하위 클래스 구현)
}

 

노드 생성입니다.

public class AddNodeCommand : ICommand
{
    public void Execute()
    {
        graph.AddNode(node);
        onAdd?.Invoke(node);  // UI 업데이트 콜백
    }
    
    public void Undo()
    {
        graph.RemoveNode(node);
        onRemove?.Invoke(node);
    }
}

Command 패턴인 이유는 Undo Redo를 구현할 계획입니다 하하...

 

자 이제 슬롯으로 가봅시다.

public class NodeSlot
{
    public string id;                // 슬롯 ID
    public string displayName;       // UI 표시명
    public SlotType slotType;        // Input/Output
    public DataType dataType;        // Texture/Float/Vector2/Vector3/Color
    public Node owner;               // 소유 노드
    public NodeSlot connectedSlot;   // 연결된 슬롯 (Input 슬롯만 사용)
}

 

슬롯 간 연결 시 타입 안정성 검사를 해야합니다.

public bool CanConnectTo(NodeSlot other)
{
    if (other.owner == this.owner) return false;        // 자기 자신 연결 방지
    if (other.slotType == this.slotType) return false;  // 같은 타입끼리 연결 불가
    if (other.dataType != this.dataType) return false;  // 데이터 타입 불일치
    return true;
}

이런식으로 슬롯을 연동할 수 있습니다잉 헤헤...

 

Slot 언제 생성함?

Slot들은 Node들 마다 필요한게 달라요... 그래서 각자 생성시에 초기화 단계에서 필요한 슬롯들을 생성합니다.

 

근데 이제 비주얼은 어디서 생성되는지

궁금할 수 있는데 Node추가 할 때!!

NodeGraphView라고 보이는 것을 총괄하는 형님이 같이 생성해줍니다!!

public NodeView(Node node)
{
    this.node = node;

    AddToClassList("node");

    headerLabel = new Label(node.nodeName);
    headerLabel.AddToClassList("node__header");
    Add(headerLabel);

    var previewImage = new Image();
    previewImage.AddToClassList("node__preview");
    previewImage.scaleMode = ScaleMode.ScaleToFit;
    Add(previewImage);

    slotsContainer = new VisualElement();
    slotsContainer.AddToClassList("node__slots");
    Add(slotsContainer);

    BuildSlots();

    RegisterCallback<MouseDownEvent>(OnMouseDown);
    RegisterCallback<MouseMoveEvent>(OnMouseMove);
    RegisterCallback<MouseUpEvent>(OnMouseUp);
    RegisterCallback<MouseLeaveEvent>(OnMouseLeave);

    schedule.Execute(() => UpdatePreview(previewImage)).Every(100);
}

 

uss

.node {
    position: absolute;
    background-color: rgb(50, 50, 50);
    border-color: rgb(80, 80, 80);
    border-width: 2px;

...

 

Node가 표시되는 과정입니다.

Node 생성 (Core Layer)
    ↓
NodeView 생성 (UI Layer)
    ↓
BuildSlots() 호출
    ↓
각 Slot을 VisualElement로 변환
    ↓
화면에 표시

 

노드 작동 로직 궁금하노 ㅋㅋ

노드 작동 원리 top 1:

[그래프 실행 요청]
               ↓
[순환 참조 검사 (Cycle Detection)]
               ↓
[토폴로지컬 정렬 (Topological Sort)]
               ↓
[의존성 순서대로 노드 실행]
               ↓
[결과 캐싱 & 출력]

순환 참조는 뭐 노드끼리 충분히 일어날 수 있기때문에 막아야합니다.

 

근데 갑자기 토폴로지컬 정렬을 사용합니다.

한국어로는 위상정렬임 ㅋㅋ;;

얘가 노드 실행 순서 정해줌.

토폴로지 정렬이라고도 합니다 음 맛있노.

참고로 얘가 순환참조까지 알아 냅니다??? 나이스~

 

설명 ㄷㄱㅈ

https://m.blog.naver.com/ndb796/221236874984

이 글을 추천할께요. 설명을 잘 함!!

일단 내 설명도함. 저거 보고오면 안 봐도 됩니다. 근데 내 설명도 괜찮음.

 

트폴로지컬 정렬

이거임.
우리 그래프 ㅎㅎ

이런 그래프가 있다고 합시다. 실행순서를 구해야하죠?

저희가 만드는 쉐이더 그래프? 도 저런식으로 생길 수 있습니다. 뭐 아웃풋은 하나긴 한데..

 

아무튼 당연하지만 처리 순서가 잘못되면 크게 결과가 바뀔 수 있습니다.

저희는 순서를 알기 쉽지만 그 컴퓨터는 그래프만 보고 순서 파악해야합니다...

이건 좀만 생각해봐도 그냥 막 실행하면 안됨..  나 설명 안 해

 

그래서 정렬 과정을 설명을 하자면

자 이건데.. 별로 안 어려워요.

이거 그림이 좀더 어지러운데 그냥 큐 무시하고 저 그림이랑 위상 순서만 보세요. 그림으로 설명합니다.

 

Input이 없는 노드, 다른말로 진입차수가 0노드들이 있죠? 처음엔 0과 1이죠.

진입 차수가 0인 노드들을 큐에 넣어줍니다. 스택으로 해도 별로 상관 없을거에요 참고로. 큐 내부에서 순서는 상관 없어요.

 

그 다음 이 큐에 있는 애들을 위상 순서에 넣어줍니다. 큐는 비우세요.

그리고 이 0과 1이랑 이어져있던 노드들의 진입 차수를 -1하세요.

쉽게 그냥 0과 1을 뺏다고 생각합니다. 쟤네에서 뻗은 간선도 없애버려요.

그럼 다음으로 진입 차수가 없는 녀석들, 2와 4죠? 이친구들을 또 같은 방식으로 위상 순서에 넣어요. 반복함.

이렇게 점점 줄여서 위상순서를 만들면 됩니다. 이게 위상 정렬입니다.

위상순서로 노드를 실행시키면 됩니다. 

 

DFS로 구현 되용

 

/// <summary>
/// Topological Sort를 사용한 실행 순서 계산
/// </summary>
private List<Node> GetExecutionOrder()
{
    var executionOrder = new List<Node>();
    var visited = new HashSet<Node>();
    var tempMarked = new HashSet<Node>();

    // 모든 노드에 대해 DFS 수행
    foreach (var node in graph.nodes)
    {
        if (!visited.Contains(node))
        {
            if (!TopologicalSortVisit(node, visited, tempMarked, executionOrder))
            {
                return new List<Node>(); // 순환 참조 발견
            }
        }
    }

    // 역순으로 반환 (의존성이 먼저 실행되도록)
    executionOrder.Reverse();
    return executionOrder;
}

여기서 DFS 출발 시킵니다.

/// <summary>
/// DFS를 사용한 Topological Sort 방문
/// </summary>
private bool TopologicalSortVisit(Node node, HashSet<Node> visited, HashSet<Node> tempMarked, List<Node> executionOrder)
{
    if (tempMarked.Contains(node))
    {
        return false; // 순환 참조 발견
    }

    if (visited.Contains(node))
    {
        return true;
    }

    tempMarked.Add(node);

    // 이 노드의 입력에 연결된 노드들을 먼저 방문
    foreach (var inputSlot in node.inputSlots)
    {
        if (inputSlot.connectedSlot != null)
        {
            var sourceNode = inputSlot.connectedSlot.owner;
            if (!TopologicalSortVisit(sourceNode, visited, tempMarked, executionOrder))
            {
                return false;
            }
        }
    }

    tempMarked.Remove(node);
    visited.Add(node);
    executionOrder.Add(node);

    return true;
}

자 이렇게 구현합니다.

 

알고리즘도 알려주는 나의 블로그 즉시 구독!!

 

중복으로 노드가 들어가면 (이미 있으면) 순환 참조임. 그래서 검사 쉬움 하하하ㅏ하핳!!

HashSet도 모른다면 그냥 c++에서 unordered_set과 동일하다.

 

참고로 노드와 슬롯에 관한 인풋VisualElements 내부 이벤트들을 이용하여 구현하였습니다.

 

노드를 선택하거나 삭제하는 것은 ISelectable IDeletable 인터페이스로 구현하였습니다. 이후에 커넥션(간선)도 추가 및 삭제가 되야하기에 인터페이스로 분리하였습니다.

 

후후ㅜ훟

일단 여기까지는 간선을 잇고 노드를 드래그 하고 드롭하는 것을 구현하였습니다. 야호!!

제 3장 인스펙터??

하하하하 인스펙터는 뭐야!!!!!

사실 이런 그래프에서는 BlackBoard라고 한다.

왼쪽엔 이거임

근데 나는 걍 인스펙터로 이름 지음 ㅠ

사용자: 노드 선택
    ↓
NodeView 클릭
    ↓
NodeGraphView.SelectNode(nodeView)
    ↓
inspector.ShowNodeProperties(nodeView.node)
    ↓
NodeInspector.ShowNodeProperties(node)
    ↓
[1] propertiesContainer.Clear() ← 기존 UI 제거
    ↓
[2] titleLabel.text = node.nodeName
    ↓
[3] previewImage 생성
    ↓
[4] 노드 타입별 분기
    │
    ├─ if (node is NoiseNode)
    │   └─ BuildNoiseNodeInspector(noiseNode)
    │
    ├─ if (node is GradientNode)
    │   └─ BuildGradientNodeInspector(gradientNode)
    │
    └─ ...
    ↓
[5] AddDeleteButton(node)

타겟층이 모바일이니 Delete버튼이 여기도 있다 ㅎㅎ!!

 

걍만듬

private void AddField<TField, TValue>(string label, TValue initialValue, Action<TValue> onValueChanged)
    where TField : BaseField<TValue>, new()
{
    var field = new TField { label = label, value = initialValue };

    // FloatField의 경우 모바일에서 너무 작은 입력값을 막기 위해 정밀도 설정
    if (field is FloatField floatField)
    {
        floatField.formatString = "F3";
    }

    field.RegisterValueChangedCallback(evt =>
    {
        onValueChanged?.Invoke(evt.newValue);
        ExecuteNode(currentNode); // currentNode를 사용하도록 변경
    });

    field.AddToClassList("inspector__field");
    propertiesContainer.Add(field);
}

// EnumField는 특별한 초기화가 필요하므로 별도 메서드
private void AddEnumField<TEnum>(string label, TEnum initialValue, Action<TEnum> onValueChanged) where TEnum : System.Enum
{
    var enumField = new EnumField(label, initialValue);

    enumField.RegisterValueChangedCallback(evt =>
    {
        if (evt.newValue is TEnum enumValue)
        {
            onValueChanged?.Invoke(enumValue);
            ExecuteNode(currentNode);
        }
    });

    enumField.AddToClassList("inspector__field");
    propertiesContainer.Add(enumField);
}

자 이건 제너릭 필드랑 enum필드다. 컬러필드도 있긴한데 뭐 여기 쓰진 않겠다.

Enum과 Color필드는 이게 유니티에 기본으로 없어서 따로 만든 것이다.

 

private void BuildNoiseNodeInspector(NoiseNode node)
{
    AddSection("Noise Properties");

    // EnumField (Type)
    AddEnumField("Noise Type", node.noiseType, newValue => node.noiseType = newValue);

    // FloatField (Scale)
    AddField<FloatField, float>("Scale", node.scale, newValue => node.scale = newValue);

    // IntegerField (Octaves)
    AddField<IntegerField, int>("Octaves", node.octaves, newValue => node.octaves = newValue);

    // FloatField (Persistence)
    AddField<FloatField, float>("Persistence", node.persistence, newValue => node.persistence = newValue);

    // Vector2Field (Offset)
    // Vector2Field는 BaseField<Vector2>를 상속하므로 AddField 사용 가능
    AddField<Vector2Field, Vector2>("Offset", node.offset, newValue => node.offset = newValue);
}

근데 이거 걍 모든 노드에 대해서 이 빌드 함수를 만들어줘야함 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

그래서 노드 늘어날때마다 이 코드들이 생성되어야한다. -> 그냥 개 문제있는 구조.

이건 내가 나중에 더 가독성있는 코드로 변경하겠다!! 이거 좀 문제 있노.

 

아무튼 빌드하기 전에 

propertiesContainer.Clear();

이걸로 컨테이너 비우고 저 필드 추가 함수로 컨테이너 채워서 Inspector를 구현하는 것이다.

아무튼 나중에 수정해보자. 더 나은 방법이 있을 것이다!!

if (node is GradientNode gradientNode)
{
    BuildGradientNodeInspector(gradientNode);
}
else if (node is NoiseNode noiseNode)
{
    BuildNoiseNodeInspector(noiseNode);
}
else if (node is ShapeNode shapeNode)
{
    BuildShapeNodeInspector(shapeNode);
}
else if (node is ConstantColorNode constantColorNode)
{
    BuildConstantColorNodeInspector(constantColorNode);
}
...

지금 현황임... ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

 

제 4장 쉐이더 연산

어 이거 쉐이더 저거 왜나오냐 어어 UI에서 쉐이더 원래 안돼는거 아니냐?? 어어어어 왜이러노... 왜이러노!!!

근데 이건 누가 한 말이고 나는 이게 뭔말인지 모른다 걍 안해봐서.

아무튼 구현해보니 잘 된다.

GPU에서 쉐이더를 연산한다.

그 후 RenderTexture로 뽑는다.

이걸 걍 UIElements.Image에 넣은 것임.

[GPU Layer]                    [UI Layer]
┌─────────────────┐           ┌─────────────────┐
│  Shader 실행    │           │  Image Element  │
│  (Compute)      │  ───────► │  (Display)      │
│                 │   RT      │                 │
│ RenderTexture   │  Texture  │  <img src=RT>   │
└─────────────────┘           └─────────────────┘

 

쉐이더 연산 과정이나 설명하겠다.

근데 Shader파일 내부나 실행과정까진 설명 안함. 걍 어케 돌아가는지만 설명할께요..

1. 단일 노드 연산

예)Noise

이건 걍 쉐이더 내부에서 Noise를 연산하여 이미지로 뽑은거임.

2. 연산노드

수학 노드가 있다. 수학 노드는 이제 인풋들을 연산해 줌.

얘네는 걍 들어온 두개의 텍스쳐 또는 뭐 텍스쳐나 숫자, 아니면 뭐 숫자끼리 더 한다. UV에 맞게.

 

그리고 연산된 것은 그 노드의 캐싱된다. 그래서 여러번 여러번 결과가 연산되서 Output 나온다. 야후!!!

5. 기타

그 Command Pattern 노드 삭제/추가 간선 연결/해제에 추가 됨.

public class CommandHistory
{
    private Stack<ICommand> undoStack = new Stack<ICommand>();
    private Stack<ICommand> redoStack = new Stack<ICommand>();

    private const int maxHistorySize = 50; // 최대 히스토리 크기

    /// <summary>
    /// 명령 실행 및 히스토리에 추가
    /// </summary>
    public void ExecuteCommand(ICommand command)
    {
        command.Execute();
        undoStack.Push(command);
        redoStack.Clear(); // 새 명령 실행 시 redo 스택 초기화

        // 최대 크기 초과 시 가장 오래된 항목 제거
        if (undoStack.Count > maxHistorySize)
        {
            var temp = new Stack<ICommand>();
            for (int i = 0; i < maxHistorySize; i++)
            {
                temp.Push(undoStack.Pop());
            }
            undoStack.Clear();
            while (temp.Count > 0)
            {
                undoStack.Push(temp.Pop());
            }
        }

        Debug.Log($"Command executed: {command.GetDescription()}");
    }

    /// <summary>
    /// Undo 실행
    /// </summary>
    public void Undo()
    {
        if (undoStack.Count == 0)
        {
            Debug.LogWarning("Nothing to undo");
            return;
        }

        var command = undoStack.Pop();
        command.Undo();
        redoStack.Push(command);

        Debug.Log($"Undo: {command.GetDescription()}");
    }

    /// <summary>
    /// Redo 실행
    /// </summary>
    public void Redo()
    {
        if (redoStack.Count == 0)
        {
            Debug.LogWarning("Nothing to redo");
            return;
        }

        var command = redoStack.Pop();
        command.Execute();
        undoStack.Push(command);

        Debug.Log($"Redo: {command.GetDescription()}");
    }

    /// <summary>
    /// Undo 가능 여부
    /// </summary>
    public bool CanUndo => undoStack.Count > 0;

    /// <summary>
    /// Redo 가능 여부
    /// </summary>
    public bool CanRedo => redoStack.Count > 0;

    /// <summary>
    /// 히스토리 초기화
    /// </summary>
    public void Clear()
    {
        undoStack.Clear();
        redoStack.Clear();
    }
}

6. 걍 더 할 일

일단 어 저거 미친 구조의 인스펙터를 바꿔야한다.

그리고 지금 OutputNode에 Texture랑 Color를 받는게 이게 걍 똑같음. 뭐 뭔차이지 어 뭐 아무튼 그렇다!!

그 다음 더 많은 노드나 이미지를 임포트해서 텍스쳐로 쓰는등 뭐시기 저시기 해야한다!!.

그리고 목표는 모바일이기 때문에 모바일 친화적인 조작을 만들어야한다!! 지금은 Undo Redo 확대 등등이 모바일에서 불가능 ㅠㅠ

 

끝끝끝끄틑긑

ㅃㅇㅃㅇ