그리기와 카메라
몹의 상자 모델은 몹 모델에서, 파티클 종류는 게임 규칙·사운드·파티클에서 다룹니다.
블록 엔티티 그리기
client.blockEntityRenderer(GRINDER, (entity, canvas) -> { // 블록 좌표 (0,0,0)~(1,1,1)
canvas.item(ResourceId.minecraft("cobblestone"), 0.5, 1.3, 0.5, 0.5f, canvas.animationTime() * 4);
canvas.text("Grinding 40%", 0.5, 1.75, 0.5, 0.02f, 0xFFFFFFFF); // 카메라를 향함
});
- 블록 엔티티가 있는 자기 모드의 블록마다 하나씩 등록합니다.
- 매 프레임 렌더 스레드에서 클라이언트 쪽 블록 엔티티를 받아 호출됩니다. 데이터가 필요하면 블록 엔티티에
syncToClients()를 켜세요. BlockEntityCanvas:item(item, x, y, z, scale, yawDegrees)(아이템 액자처럼),text(text, x, y, z, scale, argb)(카메라를 향한 글자),block(…)(블록 모델),defaultState(block),quads(pipeline, vertices),animationTime()- 예외를 던지면 해당 그리기를 끄고
POLY-CLIENT-003을 기록합니다.
블록 모델과 셰이더 사각형도 그릴 수 있습니다.
canvas.block(GLOW_BLOCK, 0.5, 2.2, 0.5, 0.35f, canvas.animationTime() * 3 % 360); // 가운데 기준, 크기, 회전
canvas.quads(glow, quads -> quads.vertex(0, 1.01, 0, 0x60FFD54F).vertex(0, 1.01, 1, 0x60FFD54F)
.vertex(1, 1.01, 1, 0x60FFD54F).vertex(1, 1.01, 0, 0x60FFD54F)); // 블록 위의 빛나는 판
block(block, x, y, z, scale, yaw)는 블록 기본 상태의 모델을 월드의 블록처럼 그립니다. 좌표는 블록의 가장 낮은 모서리를 0, 0, 0으로 합니다.
다른 상태의 모델은 BlockStateView로 그립니다. defaultState(block)이나 entity.world().stateAt(pos)로 상태를 얻은 뒤 with(속성, 값)으로 값을 바꾼 사본을 만듭니다. with는 월드를 바꾸지 않습니다.
BlockStateView lamp = canvas.defaultState(LAMP).with(LIT, true).with(COLOR, "blue");
canvas.block(lamp, 0.5, 2.7, 0.5, 0.25f, 0); // 켜진 파란 램프
canvas.block(entity.world().stateAt(entity.pos()), 0.5, 1.5, 0.5, 0.5f, 0); // 지금 상태 그대로
블록에 없는 속성이나 허용되지 않는 값을 with에 넘기면 IllegalArgumentException이 발생합니다.
월드에 그리기
client.worldPainter는 매 프레임 월드 좌표에 선, 상자, 점, 글자, 셰이더 사각형을 그립니다. 영역 표시, 경로, 디버그 보기, 빛나는 효과 등에 쓸 수 있습니다.
client.worldPainter("compass", canvas -> {
double x = canvas.cameraX(), y = canvas.cameraY() - 1.5, z = canvas.cameraZ();
canvas.line(x, y, z, x, y, z - 2, 0xFFFF5555, 3); // 북쪽으로 빨간 선, 3픽셀
canvas.text("N", x, y + 0.3, z - 2.2, 0xFFFF5555, 1); // 카메라를 보는 글자
canvas.box(x - 0.1, y - 0.1, z - 0.1, x + 0.1, y + 0.1, z + 0.1,
0xFFFFFFFF, 2, 0x40FFFFFF); // 테두리와 반투명 면
canvas.onTop().blockOutline(BlockPos.of(0, 64, 0), 0xFF55FF55, 2); // 블록에 가려지지 않음
});
WorldCanvas | 뜻 |
|---|---|
line(x1, y1, z1, x2, y2, z2, argb, width) | 선, 폭은 픽셀 |
box(min…, max…, strokeArgb, width, fillArgb) | 상자 테두리와 면. 값이 0이면 그 부분은 그리지 않음 |
blockOutline(pos, argb, width) | 블록 하나의 테두리 |
point(x, y, z, argb, size) | 점 |
text(text, x, y, z, argb, scale) | 카메라를 보는 글자 |
quads(pipeline, vertices) | 모드 셰이더 파이프라인으로 그리는 사각형 |
onTop() | 선, 상자, 점, 글자를 블록 앞에 그리는 캔버스 |
cameraX/Y/Z(), animationTime() | 카메라 위치, 게임 시간 + 부분 틱 |
- 렌더 스레드에서 매 프레임 호출되므로 가볍게 유지하세요.
- 선, 상자, 점, 글자는 게임의 기즈모(gizmo) 그리기로 그리고, 셰이더 사각형은 해당 파이프라인의 깊이 설정을 따릅니다.
- 그리기가 예외를 던지면
POLY-CLIENT-003을 기록하고 해당 그리기를 끕니다.
사용자 셰이더 파이프라인
모드가 직접 작성한 코어 셰이더(GLSL)로 사각형을 그립니다. 셰이더 파일은 assets/<namespace>/shaders/<path>.vsh와 .fsh에 둡니다.
ShaderPipeline glow = client.shaderPipeline("glow",
ShaderPipelineDefinition.builder(ResourceId.of("my_mod", "core/glow")) // shaders/core/glow.vsh, .fsh
.blend(ShaderPipelineDefinition.Blend.ADDITIVE)
.build());
canvas.quads(glow, quads -> quads
.vertex(x - 0.6, y, z - 0.6, 0x80FFD54F).vertex(x - 0.6, y, z + 0.6, 0x80FFD54F)
.vertex(x + 0.6, y, z + 0.6, 0x80FFD54F).vertex(x + 0.6, y, z - 0.6, 0x80FFD54F));
assets/my_mod/shaders/core/glow.fsh
#version 330
#moj_import <minecraft:dynamictransforms.glsl>
#moj_import <minecraft:globals.glsl>
in vec4 vertexColor;
out vec4 fragColor;
void main() {
float pulse = 0.6 + 0.4 * sin(GameTime * 2400.0); // 약 3초마다 깜빡임
fragColor = vec4(vertexColor.rgb * pulse, vertexColor.a) * ColorModulator;
}
- 정점 셰이더 입력:
in vec3 Position,in vec4 Color를 받습니다. 변환은 게임의core/position_color.vsh처럼ProjMat * ModelViewMat * vec4(Position, 1.0)로 계산합니다. - 게임 유니폼 블록:
dynamictransforms.glsl:ModelViewMat,ColorModulatorprojection.glsl:ProjMatglobals.glsl:GameTime,ScreenSize등
- 빌더 설정:
blend:OPAQUE,TRANSLUCENT(기본),ADDITIVEcull(): 뒷면을 그리지 않음noDepthTest(): 블록 앞에 그림depthWrite(): 뒤에 그려지는 것을 가림
- 처음 쓸 때 생성: 파이프라인은 처음 쓸 때 렌더 스레드에서 만들어집니다. 셰이더가 컴파일되지 않으면
POLY-CLIENT-006을 한 번만 기록하고 그 파이프라인의 사각형은 건너뜁니다. 리소스를 다시 불러오면 셰이더도 다시 읽습니다.
텍스처와 모드 유니폼
파이프라인에 텍스처를 붙이고, 모드가 정한 값을 셰이더로 넘길 수 있습니다.
AtomicLong frames = new AtomicLong();
ShaderPipeline pulse = client.shaderPipeline("pulse",
ShaderPipelineDefinition.builder(ResourceId.of("my_mod", "core/pulse"))
.texture(ResourceId.of("my_mod", "textures/item/spark.png")) // Sampler0
.uniform("Strength", ShaderUniform.Type.FLOAT,
() -> new float[] {0.75f + 0.25f * (float) Math.sin(frames.incrementAndGet() / 20.0)})
.uniform("Tint", ShaderUniform.Type.VEC4, () -> new float[] {1f, 0.85f, 0.4f, 1f})
.build());
canvas.quads(pulse, quads -> quads // 정점마다 u, v
.vertex(x - 0.3, y, z - 1.3, 0, 0, 0xFFFFFFFF).vertex(x - 0.3, y, z - 0.7, 0, 1, 0xFFFFFFFF)
.vertex(x + 0.3, y, z - 0.7, 1, 1, 0xFFFFFFFF).vertex(x + 0.3, y, z - 1.3, 1, 0, 0xFFFFFFFF));
assets/my_mod/shaders/core/pulse.fsh
#version 330
#moj_import <minecraft:dynamictransforms.glsl>
layout(std140) uniform ModUniforms { // uniform(...)을 부른 순서대로
float Strength;
vec4 Tint;
};
uniform sampler2D Sampler0;
in vec2 texCoord0;
in vec4 vertexColor;
out vec4 fragColor;
void main() {
vec4 color = texture(Sampler0, texCoord0) * vertexColor * Tint;
if (color.a == 0.0) discard;
fragColor = vec4(color.rgb * Strength, color.a) * ColorModulator;
}
- 텍스처:
texture(id)를 호출한 순서대로Sampler0,Sampler1…에 연결합니다(최대 4개). 텍스처 파일(my_mod:textures/…png)이나 동적 텍스처의id()를 쓸 수 있습니다. 파일이 없으면 게임의 누락 텍스처 무늬가 나타납니다. - 텍스처 좌표: 텍스처가 있는 파이프라인에는 정점마다
in vec2 UV0가 추가됩니다. 값은vertex(x, y, z, u, v, argb)로 넣으며,(0, 0)이 왼쪽 위입니다. 좌표 없이vertex(x, y, z, argb)를 쓰면(0, 0)이 들어갑니다. - 모드 유니폼:
uniform(이름, 형식, 값)은ModUniforms블록의 필드가 됩니다(최대 16개, 이름 중복 불가). 형식은FLOAT,VEC2,VEC3,VEC4이며, GLSL 블록의 필드 순서는 선언 순서와 같아야 합니다. - 값을 읽는 시점: 값 공급자는 매 프레임 월드를 그리기 전에 렌더 스레드에서 한 번 호출됩니다. 가볍게 유지하세요. 첫 프레임의 값은 0입니다.
- 잘못된 값: 공급자가 예외를 던지거나 값의 개수가 맞지 않으면
POLY-CLIENT-007을 한 번만 기록하고, 마지막으로 올바르게 받은 값을 계속 사용합니다. - 잘못된 선언: 이름이 GLSL 식별자가 아니거나(
gl_로 시작해도 안 됨), 개수가 한도를 넘거나, 이름이 겹치면POLY-CLIENT-002오류가 납니다.
동적 텍스처
DynamicTexture map = client.dynamicTexture("map", 64, 64); // my_mod:dynamic/map
map.setPixel(x, y, argb);
map.upload();
canvas.drawTexture(map.id(), 4, 4, 64, 64); // HUD에서
- 픽셀은 Poly 모드 로더가 보관하다가
upload()를 호출하면 클라이언트 스레드에서 게임 텍스처로 보냅니다. 텍스처는 처음 업로드할 때 만들어집니다. fill(argb)로 전체를 한 색으로 칠할 수 있습니다.- 클라이언트 스레드가 아닌 곳에서
upload()를 호출하면POLY-CLIENT-005오류가 납니다.
시야각과 시점
mod.events().register(ClientEvents.FIELD_OF_VIEW, fov -> zooming ? fov * 0.3f : fov);
view.setCameraPerspective(CameraPerspective.THIRD_PERSON_BACK); // FIRST_PERSON, THIRD_PERSON_FRONT
FIELD_OF_VIEW는 게임이 시야각(달리기, 물속)을 계산한 뒤 매 프레임 호출됩니다. 가볍게 유지하세요.- 시점 전환은 F5와 같습니다.
후처리 셰이더
boolean loaded = view.setPostEffect(ResourceId.of("my_mod", "sepia"));
// assets/my_mod/post_effect/sepia.json + assets/my_mod/shaders/post/*.fsh
view.clearPostEffect();
- 모드 리소스의
post_effect/<path>.json과 셰이더를 게임의 후처리 체인에 적용합니다. - 불러오지 못하면
false를 돌려주고, 게임이 오류를 기록합니다. - 카메라 대상이나 시점이 바뀌면 게임이 효과를 바꿉니다(크리퍼 관전 등).
사람 확인
그린 결과가 올바르게 보이는지는 사람이 직접 확인해야 합니다. 자동 검증(clientWorldReloadTest)은 파이프라인 생성, 유니폼 업로드와 연결, 상태별 블록 모델 그리기의 실행 경로만 확인합니다.