Skip to content
CTRL 섹션 중심 문서 시스템
Tooling

Maya C++ 플러그인 실습편 - MPxNode 입문과 compute 감각 잡기

MPxNode의 역할, 속성 선언, attributeAffects, MDataBlock, MDataHandle, compute 흐름을 처음 이해하는 사람을 위한 입문 가이드.

이 문서는 앞선 두 문서의 다음 단계다.

이전 문서가 MPxCommand를 통해 "Maya가 플러그인을 어떻게 로드하고 호출하는가"를 익히는 단계였다면, 이번 문서는 그 다음 질문에 답한다.

"그럼 Maya 장면 안에서 계산 단위처럼 살아 있는 커스텀 노드는 어떻게 만드는가?"

이 문서의 목표는 복잡한 디포머나 커스텀 솔버를 바로 만드는 것이 아니다.
대신 MPxNode가 어떤 역할을 하고, initialize(), attributeAffects(), compute()가 Maya 안에서 어떻게 이어지는지 처음 감각을 잡는 데 집중한다.

1. 왜 리거에게 MPxNode가 중요한가

리거는 보통 MPxCommand보다 MPxNode 쪽에서 더 큰 의미를 느끼게 된다.

이유는 간단하다.

  • 커맨드는 "한 번 실행되는 기능"에 가깝다
  • 노드는 "장면 안에 남아 있는 계산 단위"에 가깝다

리거가 관심 있는 많은 기능은 후자에 가깝다.

  • 입력 속성 몇 개를 받아서 결과를 계산한다
  • 연결을 통해 다른 노드와 관계를 맺는다
  • dirty/evaluation 흐름 안에서 다시 계산된다
  • Maya가 결과를 추적하고 저장할 수 있다

즉, MPxNode는 "새 버튼을 만드는 기술"이 아니라, Maya가 이해할 수 있는 새로운 계산 객체를 장면 안에 정의하는 기술이라고 보는 편이 맞다.

Autodesk 공식 문서도 MPxNode를 "user defined dependency nodes의 base class"라고 설명한다.
관련 읽기 시작점:

2. MPxNode를 한 문장으로 설명하면

MPxNode입력 속성을 받아 출력 속성을 계산하는 커스텀 DG 노드의 기본 클래스다.

이 문장을 세 부분으로 나눠서 이해하면 훨씬 쉽다.

1. 입력 속성을 받는다

노드는 자기 속성으로 값을 받는다.

  • float
  • int
  • bool
  • enum
  • matrix
  • string
  • mesh
  • curve

무엇이든 속성으로 선언할 수 있다.

2. 출력 속성을 계산한다

노드는 보통 입력값을 받아서 출력값을 만든다.

예를 들어:

  • 입력 distance, multiplier를 받아 outputDistance를 만든다
  • 입력 curve, parameter를 받아 outputPosition을 만든다
  • 입력 matrix들을 받아 outputMatrix를 만든다

3. DG 안에서 살아간다

이게 가장 중요하다.

노드는 단순 함수 호출과 다르게 Maya dependency graph 안에 존재한다.

  • Scene에 저장된다
  • Node Editor에 보인다
  • connection을 통해 다른 노드와 연결된다
  • 입력이 바뀌면 output이 dirty 된다
  • Maya가 필요할 때 compute()를 호출한다

이 부분이 스크립트 함수와 가장 다르다.

3. MPxCommand와 MPxNode의 차이를 다시 한 번 정리하면

입문 단계에서 이 둘은 자주 헷갈린다.

구분 MPxCommand MPxNode
중심 개념 실행 장면 안의 계산 단위
호출 방식 사용자가 명령을 실행 Maya가 output 요청 시 계산
수명 한 번 실행되고 끝나는 흐름이 많음 Scene 안에 남아 있음
대표 목적 툴 실행, 생성, 수정, 자동화 계산, 연결, 평가
리거 관점 예시 rig 생성 명령 커스텀 솔버 노드, 데이터 가공 노드

둘 다 플러그인이지만, Maya 안에서 맡는 역할은 다르다.

4. 처음 배우는 사람이 꼭 알아야 하는 핵심 용어

MPxNode를 처음 배울 때는 용어가 한꺼번에 많이 나온다.
아래 다섯 개만 먼저 정확히 잡으면 된다.

attribute

노드가 가지는 속성 정의다.
예: inputValue, multiplier, outputValue

plug

특정 노드 인스턴스의 특정 attribute 접점이다.
예: ctrlScaleNode1.inputValue

관련 공식 참고:

MDataBlock

compute() 안에서 Maya가 넘겨주는 현재 노드 인스턴스의 입력/출력 데이터 묶음이다.
data block은 compute() 실행 중에만 유효하다고 이해하면 된다.

MDataHandle

MDataBlock 안의 특정 attribute 값을 읽거나 쓰기 위한 핸들이다.

attributeAffects

어떤 입력이 어떤 출력에 영향을 주는지 Maya에게 알려주는 선언이다.
이 선언이 있어야 dirty propagation과 compute() 호출 흐름이 기대대로 이어진다.

5. MPxNode의 생명주기

처음에는 compute()만 보면 안 된다.
노드는 보통 아래 순서로 이해하는 편이 좋다.

  1. 플러그인 로드
  2. registerNode() 호출
  3. initialize() 실행
  4. Maya가 노드 타입을 알게 됨
  5. createNode 또는 다른 생성 경로로 인스턴스 생성
  6. creator() 호출
  7. 필요할 때 compute() 호출
  8. 플러그인 언로드 시 deregisterNode()

Autodesk의 dependency node 예제도 creator(), initialize(), initializePlugin(), uninitializePlugin(), compute()를 기본 구성으로 보여준다.

이 순서를 감으로 잡아두면, 노드 코드가 훨씬 덜 막연해진다.

6. 가장 작은 MPxNode 예제

처음 예제는 단순해야 한다.
입력 float 2개를 받아 결과 float 1개를 내는 노드가 좋다.

아래 예제는 inputValue * multiplier를 계산해서 outputValue로 내보내는 가장 기본적인 형태다.

#include <maya/MFnNumericAttribute.h>
#include <maya/MFnNumericData.h>
#include <maya/MFnPlugin.h>
#include <maya/MDataBlock.h>
#include <maya/MDataHandle.h>
#include <maya/MPxNode.h>
#include <maya/MPlug.h>
#include <maya/MStatus.h>
#include <maya/MTypeId.h>

class CtrlScaleNode : public MPxNode
{
public:
    static MTypeId id;
    static MObject inputValue;
    static MObject multiplier;
    static MObject outputValue;

    static void* creator()
    {
        return new CtrlScaleNode();
    }

    static MStatus initialize()
    {
        MFnNumericAttribute nAttr;

        inputValue = nAttr.create("inputValue", "in", MFnNumericData::kFloat, 0.0f);
        nAttr.setKeyable(true);
        addAttribute(inputValue);

        multiplier = nAttr.create("multiplier", "mul", MFnNumericData::kFloat, 1.0f);
        nAttr.setKeyable(true);
        addAttribute(multiplier);

        outputValue = nAttr.create("outputValue", "out", MFnNumericData::kFloat, 0.0f);
        nAttr.setWritable(false);
        nAttr.setStorable(false);
        addAttribute(outputValue);

        attributeAffects(inputValue, outputValue);
        attributeAffects(multiplier, outputValue);

        return MS::kSuccess;
    }

    MStatus compute(const MPlug& plug, MDataBlock& data) override
    {
        if (plug != outputValue)
        {
            return MS::kUnknownParameter;
        }

        float input = data.inputValue(inputValue).asFloat();
        float factor = data.inputValue(multiplier).asFloat();

        MDataHandle outHandle = data.outputValue(outputValue);
        outHandle.setFloat(input * factor);

        data.setClean(plug);
        return MS::kSuccess;
    }
};

MTypeId CtrlScaleNode::id(0x0007F7A1);
MObject CtrlScaleNode::inputValue;
MObject CtrlScaleNode::multiplier;
MObject CtrlScaleNode::outputValue;

MStatus initializePlugin(MObject obj)
{
    MFnPlugin plugin(obj, "CTRL", "1.0.0", "Any");
    return plugin.registerNode(
        "ctrlScaleNode",
        CtrlScaleNode::id,
        CtrlScaleNode::creator,
        CtrlScaleNode::initialize,
        MPxNode::kDependNode
    );
}

MStatus uninitializePlugin(MObject obj)
{
    MFnPlugin plugin(obj);
    return plugin.deregisterNode(CtrlScaleNode::id);
}

이 예제는 구조를 설명하기 위한 것이므로, 처음에는 코드 길이보다 각 줄의 역할을 이해하는 쪽이 중요하다.

7. 위 예제에서 꼭 봐야 하는 것

MTypeId

노드 타입 ID다.
모든 dependency node는 type ID가 필요하다.

Autodesk 문서 기준으로, 내부 전용 노드라면 일정 범위 안의 값을 쓸 수 있지만 외부 배포 목적이라면 정식 ID range를 받아야 한다.

처음에는 이렇게 이해하면 된다.

  • 로컬 실험: 내부 규칙 안에서 관리
  • 외부 배포: ID 충돌을 피하기 위해 정식 관리 필요

static MObject

이 값들은 "노드 타입이 가지는 attribute 정의"를 가리킨다.
인스턴스별 값 자체가 아니다.

입문자는 여기서 자주 헷갈린다.

  • inputValue 자체는 attribute 정의
  • ctrlScaleNode1.inputValue 의 실제 숫자값은 인스턴스 데이터

실제 숫자값은 compute() 안에서 MDataBlock을 통해 읽는다.

initialize()

여기서 attribute를 선언하고 addAttribute()attributeAffects()를 설정한다.

즉, initialize()는 "이 노드 타입은 어떤 입출력 접점을 가질 것인가"를 Maya에게 알려주는 함수다.

creator()

Maya가 노드 인스턴스를 실제로 만들 때 호출한다.

즉, initialize()가 타입 설계라면, creator()는 인스턴스 생성이다.

compute()

여기가 노드의 본체다.
Maya는 output plug가 dirty 되어 다시 계산이 필요할 때 compute()를 호출한다고 이해하면 된다.

8. attributeAffects()가 왜 중요한가

처음에는 그냥 "필수로 쓰는 선언"처럼 보이지만, 사실 이 줄이 Maya에게 인과관계를 알려준다.

attributeAffects(inputValue, outputValue);
attributeAffects(multiplier, outputValue);

이 선언의 의미는 다음과 같다.

  • inputValue가 바뀌면 outputValue는 더 이상 최신 값이 아니다
  • multiplier가 바뀌면 outputValue도 다시 계산해야 한다

Autodesk 문서도 attributeAffects()를 입력과 출력 사이의 관계를 알려주는 선언으로 다룬다.

이게 없으면 초보자 기준으로 가장 흔한 문제가 생긴다.

  • compute()가 안 불리는 것처럼 보인다
  • output 값이 안 바뀌는 것처럼 보인다
  • 연결은 했는데 갱신이 안 되는 것처럼 느껴진다

즉, attributeAffects()는 단순 문법이 아니라 Maya evaluation 시스템에게 dirty 전파 규칙을 알려주는 선언이다.

9. compute()에서 꼭 지켜야 하는 규칙

Autodesk 공식 문서는 compute() 구현에서 몇 가지를 강하게 강조한다.

1. 요청된 plug를 먼저 확인한다

공식 예제와 레퍼런스도 모르는 plug에 대해서는 MS::kUnknownParameter를 반환하는 패턴을 사용한다.

그래서 보통 이렇게 시작한다.

if (plug != outputValue)
{
    return MS::kUnknownParameter;
}

이 습관이 있어야 불필요한 계산과 이상한 호출 흐름을 줄일 수 있다.

2. input은 data.inputValue()로 읽는다

예:

float input = data.inputValue(inputValue).asFloat();

3. output은 data.outputValue()로 쓴다

예:

MDataHandle outHandle = data.outputValue(outputValue);
outHandle.setFloat(result);

Autodesk의 data block 문서도 input/output handle을 이런 식으로 읽고 쓰는 패턴을 제시한다.

4. 계산이 끝나면 clean으로 표시한다

이 부분은 정말 중요하다.

data.setClean(plug);

계산한 output plug는 반드시 clean으로 표시해야 한다.

이걸 빼먹으면:

  • 계속 다시 계산되는 것처럼 보이거나
  • refresh 때마다 이상하게 재호출되거나
  • 디버깅이 어려운 상태가 된다

5. compute() 안에서 바깥세상을 직접 바꾸지 않는다

공식 문서와 예제 흐름도 node가 자기 output 외부의 값을 직접 바꾸지 않는 방향을 전제로 한다.

즉, compute()는 가능한 한 아래 원칙을 지켜야 한다.

  • 입력만 읽는다
  • 출력만 쓴다
  • 다른 노드를 직접 수정하지 않는다
  • DAG를 직접 만지지 않는다

이 원칙이 깨지면 evaluation 루프를 이해하기 매우 어려워진다.

10. MDataBlockMDataHandle을 어떻게 느끼면 좋은가

처음에는 이름이 어렵지만, 감각적으로 보면 이렇다.

  • MDataBlock: 지금 이 노드 인스턴스의 현재 계산용 작업 테이블
  • MDataHandle: 그 테이블 안의 특정 칸을 읽고 쓰는 손잡이

Autodesk 문서도 data block은 compute() 실행 중에만 유효하며, 포인터를 저장해두면 안 된다고 설명한다.

그래서 초보자에게 중요한 규칙은 이것뿐이다.

  1. 필요한 값은 compute() 안에서 다시 읽는다
  2. MDataBlock이나 내부 포인터를 멤버 변수처럼 오래 들고 있지 않는다

11. 이 노드를 Maya에서 어떻게 시험해보는가

빌드와 로드는 이전 Windows 실습편과 동일하다.

로드 후에는 Script Editor에서 노드를 직접 생성해볼 수 있다.

import maya.cmds as cmds

cmds.loadPlugin(r"C:\maya-dev\plugins\ctrlScaleNode.mll")
node = cmds.createNode("ctrlScaleNode")

cmds.setAttr(f"{node}.inputValue", 2.0)
cmds.setAttr(f"{node}.multiplier", 5.0)

result = cmds.getAttr(f"{node}.outputValue")
print(result)

정상이라면 10.0 이 나온다.

이 실험이 중요한 이유는 MPxNode가 생각보다 추상적인 개념이 아니라, 만들고 나면 Maya 장면 안에서 일반 노드처럼 다룰 수 있는 객체라는 걸 바로 체감하게 해주기 때문이다.

12. 초반에 가장 많이 하는 실수

1. attributeAffects()를 빼먹는다

증상:

  • input을 바꿨는데 output이 갱신되지 않는다
  • compute()가 안 불리는 것처럼 보인다

2. data.setClean(plug)를 빼먹는다

증상:

  • 계속 dirty 한 것처럼 보인다
  • 평가가 불안정하게 느껴진다

3. input/output 타입을 다르게 읽고 쓴다

Autodesk 문서도 handle에서 읽고 쓰는 타입은 attribute 타입과 맞아야 한다고 설명한다.

예를 들어 float attribute면:

  • asFloat() 로 읽고
  • setFloat() 로 쓰는 편이 안전하다

4. output attribute를 writable 하게 두거나 storable 하게 둔다

계산 결과용 output은 보통:

  • setWritable(false)
  • setStorable(false)

로 두는 편이 자연스럽다.

왜냐하면 사용자가 직접 써넣는 값이 아니라 계산 결과이기 때문이다.

5. 노드와 커맨드의 역할을 혼동한다

노드 안에서 "장면 생성 명령 전체"를 처리하려고 들면 금방 복잡해진다.

보통은 이렇게 분리하는 편이 좋다.

  • 커맨드: 생성, 배치, UI 호출
  • 노드: 계산

이 구조가 실무에서 유지보수하기 좋다.

13. MPxNode만 있는 것은 아니다

Autodesk 문서가 보여주듯, MPxNode는 base class이고 그 위에 특수화된 노드 계층이 많다.

예를 들어:

  • MPxLocatorNode
  • MPxDeformerNode
  • MPxTransform
  • MPxIkSolverNode
  • MPxManipulatorNode

즉, 모든 것을 무조건 base MPxNode에서 시작하는 것은 아니다.

하지만 입문자는 먼저 base MPxNode의 공통 규칙을 이해하는 것이 좋다.

  • attribute 선언
  • attributeAffects()
  • compute()
  • data block
  • clean/dirty

이 공통 규칙을 이해한 뒤에 특수 노드 클래스로 가는 편이 훨씬 덜 흔들린다.

14. 리거 기준으로 어떤 노드부터 만들어보면 좋은가

처음 실습용 노드는 "작지만 노드다운 것"이 좋다.

추천 예시:

  • float 두 개를 받아 결과를 내는 곱셈 노드
  • angle 보정값을 계산하는 노드
  • 두 matrix나 distance 값을 받아 보조 출력값을 만드는 노드
  • 리그 검사용 상태 계산 노드

처음부터 아래로 가면 어렵다.

  • 커스텀 deformer
  • custom transform
  • viewport locator
  • solver
  • geometry data 생성 노드

왜냐하면 개념이 많아지기 때문이다.

처음에는 "입력 2개, 출력 1개, compute 1개" 정도가 가장 좋다.

15. 다음 단계로 무엇을 하면 좋은가

MPxNode 입문 다음에는 보통 아래 순서가 좋다.

1. numeric attribute만 쓰는 노드를 여러 개 만들어본다

  • float
  • bool
  • enum
  • vector

이 단계에서 attribute 선언 감각이 생긴다.

2. 연결을 적극적으로 테스트한다

  • 다른 Maya 노드와 연결
  • 값 변경 시 dirty propagation 확인
  • Node Editor에서 관계 보기

이 단계에서 "왜 노드가 Maya스럽다고 하는지"가 보이기 시작한다.

3. setDependentsDirty()는 나중에 본다

고급 단계에서는 dynamic attribute나 더 복잡한 dirty 관계를 위해 setDependentsDirty()가 필요할 수 있다.
Autodesk의 MPxNode 레퍼런스도 attributeAffects()로 처리되지 않는 dynamic attribute 관계는 setDependentsDirty()로 다루라고 안내한다.

하지만 입문 단계에서는 먼저 정적 attribute와 attributeAffects()만으로 충분하다.

4. 그다음에 MPxNode 기반 커스텀 리깅 노드로 확장한다

이제부터는 진짜 리거다운 주제가 열린다.

  • pose 보정
  • constraint 보조 계산
  • matrix 가공
  • space switching 보조 데이터
  • custom solver 전 단계 프로토타입

16. 이 문서의 체크리스트

이 문서를 읽고 아래가 설명되면 성공이다.

  • MPxNode가 커맨드와 어떻게 다른지
  • initialize()creator()가 각각 무엇을 하는지
  • attributeAffects()가 왜 필요한지
  • compute()가 언제 호출되는지
  • MDataBlockMDataHandle을 어디서 쓰는지
  • data.setClean(plug)가 필요한지

이 감각이 잡히면, 다음부터는 코드를 외우는 것이 아니라 Maya가 어떤 시점에 무엇을 기대하는지를 기준으로 노드 코드를 읽을 수 있게 된다.

그 시점부터 MPxNode는 덜 무섭고, 훨씬 Maya답게 느껴진다.