NeoForge 26.1 Docs
  • 문서
  • 노트
NeoForge 26.1
NeoForge 26.1
v26.1
학습 진도
0 / 77 챕터 완료방문 0
BlockEntity 기초 — CounterBlockEntityBlockEntityRenderer 구현Entity 등록과 AttributeSupplierEntityRenderer와 EntityModelAI Goal 시스템CustomPacketPayload — 클라이언트/서버 패킷 통신AttachmentTypes — 엔티티·블록에 데이터 부착WorldGen Feature 기초 — 광물 생성
고급 시스템

EntityRenderer와 EntityModel

MobRenderer와 EntityModel을 상속해 커스텀 Mob의 3D 렌더러와 모델 레이어를 구현하고, 클라이언트 이벤트로 안전하게 등록하는 방법을 배웁니다.

EntityRenderer란?

엔티티가 월드에 존재한다면, 플레이어 화면에 보이도록 렌더링해야 합니다. NeoForge(Minecraft)는 Vulkan 기반 렌더 백엔드를 사용하지만, 모드 API는 EntityRenderer → MobRenderer → EntityModel 계층을 통해 백엔드 독립 방식으로 렌더링을 처리합니다.

이번 챕터에서는:

  1. EntityModel — 메시(큐브) 정의 + UV 매핑
  2. MobRenderer — 텍스처 연결 + 렌더 등록
  3. 클라이언트 전용 이벤트 — RegisterLayerDefinitions + RegisterRenderers

세 가지를 단계적으로 구현합니다.


1단계 — EntityModel 구현

EntityModel은 엔티티의 **3D 형태(메시)**를 정의합니다. 각 큐브(cube)는 ModelPart로 표현되며, 전체 UV 크기(텍스처 해상도)를 지정해야 합니다.

examplemod-template-26.1.2/src/main/java/com/example/examplemod/client/model/RubyGolemModel.java

public class RubyGolemModel extends EntityModel<LivingEntityRenderState> {
 
    /**
     * 레이어 위치 — RegisterLayerDefinitions 이벤트에서 이 키로 등록합니다.
     * 26.1.2 에서 ResourceLocation 은 삭제되어 Identifier 를 사용합니다.
     */
    public static final ModelLayerLocation LAYER_LOCATION =
            new ModelLayerLocation(
                    Identifier.fromNamespaceAndPath("examplemod", "ruby_golem"), "main");
 
    private final ModelPart head;
    private final ModelPart body;
 
    public RubyGolemModel(ModelPart root) {
        super(root);
        this.head = root.getChild("head");
        this.body = root.getChild("body");
    }
 
    /**
     * LayerDefinition 팩토리 — 큐브 메시와 텍스처 해상도를 정의합니다.
     * RegisterLayerDefinitions 이벤트에서 이 메서드 참조를 전달합니다.
     */
    public static LayerDefinition createBodyLayer() {
        MeshDefinition mesh = new MeshDefinition();
        PartDefinition root = mesh.getRoot();
 
        // 머리: 8×8×8 큐브, 피벗 (0, 0, 0), UV 오프셋 (0, 0)
        root.addOrReplaceChild("head",
                CubeListBuilder.create()
                        .texOffs(0, 0)
                        .addBox(-4.0f, -8.0f, -4.0f, 8.0f, 8.0f, 8.0f),
                PartPose.offset(0.0f, 0.0f, 0.0f));
 
        // 몸통: 8×12×4 큐브, 피벗 (0, 0, 0), UV 오프셋 (16, 16)
        root.addOrReplaceChild("body",
                CubeListBuilder.create()
                        .texOffs(16, 16)
                        .addBox(-4.0f, 0.0f, -2.0f, 8.0f, 12.0f, 4.0f),
                PartPose.offset(0.0f, 0.0f, 0.0f));
 
        // 텍스처 해상도: 64×32 (UV 좌표 계산 기준)
        return LayerDefinition.create(mesh, 64, 32);
    }
 
    /**
     * setupAnim — 26.1.2 에서 인자는 RenderState 하나뿐이다.
     * 엔티티 인스턴스에는 접근할 수 없으며, 회전·보행 등 모든 값은
     * 렌더러의 extractRenderState 에서 RenderState 로 추출되어 전달된다.
     * state.yRot / state.xRot 는 LivingEntityRenderer 가 채워준다.
     */
    @Override
    public void setupAnim(LivingEntityRenderState state) {
        super.setupAnim(state);
        // 머리 회전 — 엔티티가 바라보는 방향에 맞춤
        this.head.yRot = state.yRot * ((float) Math.PI / 180f);
        this.head.xRot = state.xRot * ((float) Math.PI / 180f);
    }
}

핵심 포인트:

요소설명
LAYER_LOCATION레이어 등록·조회 키 (Identifier + 슬롯 이름)
createBodyLayer()정적 팩토리 — LayerDefinition.create(mesh, uvW, uvH)
addOrReplaceChild(...)큐브 이름·크기·오프셋·UV를 한 번에 정의
setupAnim(LivingEntityRenderState)매 프레임 호출 — 26.1.2 에서 단일 RenderState 인자. 걷기 사이클·머리 회전 등 구현 위치
렌더링베이스 Model#renderToBuffer(final)가 담당 — 직접 오버라이드하지 않음

2단계 — MobRenderer 구현

MobRenderer는 EntityRenderer의 하위 클래스로, 텍스처 경로를 제공하고 적절한 모델을 연결합니다.

examplemod-template-26.1.2/src/main/java/com/example/examplemod/client/renderer/RubyGolemRenderer.java

public class RubyGolemRenderer
        extends MobRenderer<RubyGolemEntity, RubyGolemRenderState, RubyGolemModel> {
 
    /**
     * 텍스처 경로 — assets/<modId>/textures/entity/<name>.png
     * 26.1.2 에서 ResourceLocation 은 삭제되어 Identifier 를 사용하며,
     * Identifier 는 "textures/" prefix 를 자동으로 붙여주지 않습니다.
     * 반드시 "textures/entity/..." 형태로 명시해야 합니다.
     */
    private static final Identifier TEXTURE =
            Identifier.fromNamespaceAndPath("examplemod", "textures/entity/ruby_golem.png");
 
    public RubyGolemRenderer(EntityRendererProvider.Context context) {
        super(context,
              new RubyGolemModel(context.bakeLayer(RubyGolemModel.LAYER_LOCATION)),
              0.5f);  // 그림자 반지름 (블록 단위)
    }
 
    /**
     * 매 프레임 재사용할 RenderState 를 생성합니다. (abstract — 반드시 구현)
     */
    @Override
    public RubyGolemRenderState createRenderState() {
        return new RubyGolemRenderState();
    }
 
    /**
     * 엔티티 상태를 RenderState 로 추출합니다.
     * 추가 커스텀 필드가 없으므로 부모 구현(표준 LivingEntity 필드 채움)만으로 충분합니다.
     * 커스텀 애니메이션 값이 생기면 super 호출 뒤 state.xxx = entity.xxx 로 채웁니다.
     */
    @Override
    public void extractRenderState(RubyGolemEntity entity, RubyGolemRenderState state, float partialTick) {
        super.extractRenderState(entity, state, partialTick);
    }
 
    @Override
    public Identifier getTextureLocation(RubyGolemRenderState state) {
        return TEXTURE;
    }
}

생성자 파라미터:

파라미터설명
context레이어 베이크·폰트 등 렌더 컨텍스트
new RubyGolemModel(context.bakeLayer(...))레이어를 구워 모델 인스턴스 생성
0.5f그림자 반지름 (블록 단위) — 0이면 그림자 없음

26.1.2 엔티티 렌더 리팩터로 MobRenderer는 <엔티티, RenderState, 모델> 3-제네릭 형태가 되었습니다. createRenderState()(abstract)로 매 프레임 재사용할 스냅샷을 만들고, extractRenderState(...)에서 엔티티 값을 그 스냅샷으로 복사합니다.

getTextureLocation()은 RenderState에 따라 동적 텍스처를 반환할 수도 있습니다. 예를 들어 HP 기반으로 다른 텍스처를 쓰고 싶다면 extractRenderState에서 HP를 RenderState로 복사한 뒤 이 메서드에서 분기합니다.


3단계 — 클라이언트 이벤트로 등록

렌더러와 레이어 등록은 클라이언트 전용 이벤트입니다. 서버에서 절대 실행되어서는 안 됩니다.

examplemod-template-26.1.2/src/main/java/com/example/examplemod/client/ClientEventHandler.java

@EventBusSubscriber(modid = "examplemod", value = Dist.CLIENT)
public class ClientEventHandler {
 
    /**
     * 1단계: 레이어 정의 등록 — 모델의 큐브 메시를 등록합니다.
     */
    @SubscribeEvent
    public static void registerLayer(EntityRenderersEvent.RegisterLayerDefinitions event) {
        event.registerLayerDefinition(
                RubyGolemModel.LAYER_LOCATION,
                RubyGolemModel::createBodyLayer);
    }
 
    /**
     * 2단계: 렌더러 등록 — 엔티티 타입에 렌더러를 연결합니다.
     */
    @SubscribeEvent
    public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
        event.registerEntityRenderer(
                ModEntities.RUBY_GOLEM.get(),
                RubyGolemRenderer::new);
    }
}

value = Dist.CLIENT가 핵심입니다. 이 어노테이션이 없으면 전용 서버(Dedicated Server)에서도 클래스가 로드되어 NoClassDefFoundError가 발생합니다.


4단계 — 텍스처 파일 배치

렌더러가 참조하는 텍스처는 리소스 팩 경로 규칙을 따릅니다.

src/main/resources/
└── assets/
    └── examplemod/
        └── textures/
            └── entity/
                └── ruby_golem.png   ← 64×32 권장

Identifier.fromNamespaceAndPath("examplemod", "textures/entity/ruby_golem.png")는 이 경로를 직접 참조합니다.

UV 매핑 기준: LayerDefinition.create(mesh, 64, 32)에서 지정한 64×32가 UV 계산의 기준 해상도입니다. 실제 PNG 파일도 동일 비율(64×32, 128×64, 256×128 등)로 제작해야 합니다.


안티패턴

⚠️ 텍스처 누락 → 검정/보라 렌더링

@Override
public Identifier getTextureLocation(RubyGolemRenderState state) {
    // ❌ 잘못된 경로 — "textures/" prefix 빠짐
    return Identifier.fromNamespaceAndPath("examplemod", "ruby_golem.png");
}

Identifier는 textures/ prefix를 자동으로 붙여주지 않습니다.
경로는 assets/<modId>/ 이후의 전체 경로를 명시해야 합니다.

✅ 정답: "textures/entity/ruby_golem.png"


⚠️ 클라이언트 분리 누락 → 전용 서버 크래시

// ❌ Dist.CLIENT 없음 — 서버에서도 로드됨
@EventBusSubscriber(modid = "examplemod")
public class ClientEventHandler {
    @SubscribeEvent
    public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) { ... }
}

렌더러 이벤트 클래스는 반드시 value = Dist.CLIENT로 클라이언트 한정 로딩해야 합니다.
서버에는 EntityRenderer 클래스 자체가 없어 NoClassDefFoundError로 크래시합니다.

✅ 정답: @EventBusSubscriber(..., value = Dist.CLIENT)


Vulkan 백엔드와 모드 API

NeoForge 26.1은 Vulkan 기반 렌더 백엔드를 사용합니다. 그러나 모드는 직접 GL/Vulkan 호출을 할 필요가 없습니다:

  • PoseStack — 변환 행렬 스택 (GL glPushMatrix 대체)
  • MultiBufferSource — 렌더 버퍼 추상화 (Vulkan 커맨드 버퍼 위에 위치)
  • VertexConsumer — 정점 데이터 기록 인터페이스

베이스 Model#renderToBuffer(PoseStack, VertexConsumer, ...)(26.1.2 에서 final)가 각 ModelPart를 그려주며, Vulkan 렌더러가 알아서 처리합니다. 직접 GL 함수(glVertexAttribPointer 등)를 호출하는 것은 금지입니다 — 백엔드 교체 시 즉시 깨집니다.


체크리스트

  • RubyGolemModel.LAYER_LOCATION 정의 + createBodyLayer() 구현
  • RubyGolemRenderer.TEXTURE 경로가 "textures/entity/ruby_golem.png" 형식
  • ClientEventHandler에 value = Dist.CLIENT 어노테이션 존재
  • registerLayer → RegisterLayerDefinitions 이벤트에서 처리
  • registerRenderers → RegisterRenderers 이벤트에서 처리
  • assets/examplemod/textures/entity/ruby_golem.png 파일 존재 (64×32 이상)
  • 인게임 /summon examplemod:ruby_golem ~ ~ ~ 후 텍스처가 보임 (검정/보라 아님)

정리

  • EntityModel — createBodyLayer()로 큐브 메시·UV·피벗을 정의. LAYER_LOCATION이 등록 키.
  • MobRenderer — getTextureLocation()이 텍스처를 반환. 생성자에서 context.bakeLayer()로 모델을 굽는다.
  • 등록은 클라이언트 이벤트 버스에서: RegisterLayerDefinitions → RegisterRenderers 순서.
  • Dist.CLIENT 없이 서버에 배포하면 크래시 — 반드시 명시.
  • Vulkan 환경에서도 PoseStack + MultiBufferSource + VertexConsumer API만 사용. 직접 GL 호출 금지.

다음 챕터에서는 AI Goal 시스템으로 엔티티의 자율 행동 패턴을 구현합니다.

Entity 등록과 AttributeSupplier

DeferredRegister로 커스텀 Mob 엔티티 타입을 등록하고, EntityAttributeCreationEvent로 AttributeSupplier를 안전하게 연결하는 방법을 배웁니다.

AI Goal 시스템

registerGoals 오버라이드로 커스텀 Mob에 FloatGoal, MeleeAttackGoal, WaterAvoidingRandomStrollGoal 등 Vanilla Goal을 추가하고, goalSelector와 targetSelector의 우선순위 체계를 이해합니다.

On this page

EntityRenderer란?1단계 — EntityModel 구현2단계 — MobRenderer 구현3단계 — 클라이언트 이벤트로 등록4단계 — 텍스처 파일 배치안티패턴Vulkan 백엔드와 모드 API체크리스트정리
NeoForge 26.1 Docs

NeoForge 26.1 모딩 개발 문서 사이트

GitHubDiscord

문서

  • 문서
  • 노트

GitHub

  • GitHub
  • Discord

© 2026 NeoForge 26.1 Docs. 콘텐츠는 MIT 라이선스로 제공됩니다.

Built with Next.js · Tailwind CSS · shadcn/ui