Blender에서 로우폴리 모델링
로우폴리는 스타일입니다. 단순히 낮은 삼각형 개수가 아니라, 크고 읽기 쉬운 형태, 눈에 띄는 평평한 면, 몇 가지 단색, 그리고 현실보다 약간 과장된 비율입니다. 폴리곤 수가 적은 사실적인 모델은 로우폴리가 아닙니다. 그것은 거친 사실적 모델일 뿐입니다.
일반적인 작업 루틴(씬을 보고, 부분별로 계획하고, 호출당 한 부분씩 작업하고, 각 단계 후 스크린샷을 찍고, 자신이 만들지 않은 것은 절대 삭제하지 않음)은 blender-modeling 스킬에 있습니다. 그것을 따르세요. 이 스킬은 로우폴리에서 무엇이 다른지 설명합니다.
Blender 연결
BlenderLab을 통해 작업합니다. 공식 Blender Lab 서버이며, 해당 도구는 blenderlab_…입니다:
- 씬에 있는 것:
get_objects_summary; 하나의 오브젝트를 자세히:get_object_detail_summary(name으로). 파일(저장 여부 및 경로):get_blendfile_summary_path_info. - Python 실행:
execute_blender_code. 호출 사이에 아무것도 유지되지 않습니다(각 스크립트에 도우미 함수를 포함시키세요). 스크립트가 출력하는 내용이 반환되고,result에 할당된 딕셔너리가 데이터로 반환됩니다. 가드는 일부 연산자(Blender 종료, 공장 초기화)를 거부합니다. - 3D 뷰포트 보기:
get_screenshot_of_area_as_image(witharea_ui_type: "VIEW_3D"); 전체 창:get_screenshot_of_window_as_image); 오브젝트를 가리키기:jump_to_view3d_object_by_name. - Python API 확인:
get_python_api_docs(bpy.types.BevelModifier와 같은 식별자),search_api_docs; 매뉴얼:search_manual_docs. - 빠른 렌더링:
render_thumbnail_to_path,render_viewport_to_path; 이미지는 Blender의 임시 폴더로 이동하고 결과가 실제 경로를 제공합니다. - 내보내기: 코드 (
bpy.ops.export_scene.gltf,bpy.ops.export_scene.fbx). - 에셋 라이브러리 및 3D 생성기: 없음. 오브젝트를 직접 만들거나, 사용자에게 가져올 파일을 요청하세요.
이러한 도구를 사용할 수 없으면 중단하고 그렇게 말하세요. 사용자는 BlenderLab 연결(도구 → 연결)을 설치하고, 이 에이전트에 제공하고, 애드온의 서버가 시작된 상태로 Blender를 열어야 합니다. 할 수 없는 작업을 완료된 것으로 설명하지 마세요.
스타일, 규칙으로
- 모든 곳에서 플랫 쉐이딩. 모든 면은 하나의 평평한 색상입니다. 부드러운 쉐이딩이나 자동 스무스가 없습니다. 이것이 면이 보이게 만드는 것입니다.
- 의도적으로 적은 세그먼트. 실린더는 6-8면, 원뿔은 5-6면, 구는 1-2단계 세분화된 아이코스피어로 만듭니다. 32면 실린더는 무엇보다도 룩을 망칩니다.
- 베벨, 서브디비전 서페이스 없음. 면을 둥글게 만듭니다. 더 부드러운 가장자리가 필요하면 최대 단일 세그먼트 모따기를 사용하세요.
- 단색, 텍스처 없음. 색상당 하나의 재질, Base Color가 있는 Principled BSDF, Roughness 0.8-1.0, Metallic 0, 스페큘러 광택 없음. 재질을 재사용하세요. 전체 씬에는 보통 6-12개가 필요합니다.
- 의도적인 팔레트. 모델링 전에 선택하세요: 1-2개의 지배적인 색상, 2-3개의 보조 색상, 1개의 악센트 색상. 약간 채도가 낮은 중간톤, 따뜻함과 차가움의 균형, 그리고 이웃하는 부분 간의 다양성을 위해 각 주요 색상의 두 가지 톤(밝음과 어두움).
- 통통하고 과장된 비율. 실제보다 두꺼운 벽, 기둥, 줄기; 과도하게 큰 지붕, 문, 창문; 테이퍼진 형태(밑부분이 더 넓거나, 카툰 느낌을 위해 윗부분이 더 넓음). 이 수준의 디테일에서는 얇은 부분이 사라집니다.
- 손으로, 소량으로 불완전함 만들기. 완벽하게 정렬된 것은 없습니다: 굴뚝을 2-4° 기울이고, 기와의 크기를 몇 퍼센트, 기울기를 2-3° 변형하고, 바위와 지형의 정점을 무작위로 이동시킵니다. 구조는 여전히 읽을 수 있어야 합니다: 기와 줄은 줄로 유지되고, 어떤 조각도 옆으로 돌리거나 표면에서 들어 올리지 않습니다. 고정된 무작위 시드를 사용하여 재실행 시 동일한 결과를 얻습니다.
- 세분화가 아닌 별도의 작은 조각을 통한 디테일. 창문은 네 개의 상자로 만든 프레임에 유리 한 장; 기와는 작은 슬래브의 줄; 돌은 벽에 박힌 몇 개의 불규칙한 블록입니다. 각 조각은 단순합니다. 배열이 풍부함을 제공합니다.
- 조각은 교차할 수 있음. 로우폴리 부품은 용접되는 대로 서로 밀어 넣습니다: 빔이 벽에 파고들고, 바위가 땅에 파고듭니다. 불리언이나 깔끔한 접합에 노력을 들이지 마세요.
워크플로우
- 참조가 있으면 읽으세요: 부품을 나열하고, 반복되는 것을 세고(기와 줄, 빔 개수, 벽당 창문 수), 부품 간의 비율을 비율로 기록하고(벽 높이에 대한 지붕 높이, 너비에 대한 높이), 팔레트 색상의 이름을 지정하세요. 그 목록을 기반으로 만드세요. 이미지가 결정합니다: 하나의 크기를 기준으로 삼고 비율에서 나머지를 도출하고, 그림과 모순되는 미터 단위의 크기를 발명하지 말고, 요청하지 않는 한 그림에 표시되지 않은 것(베이스, 소품)을 추가하지 마세요.
- 블록아웃 큰 덩어리를 먼저 올바른 비율의 상자와 쐐기로 만듭니다. 스크린샷을 찍고 실루엣을 비교하세요: 실루엣이 잘못되면 디테일이 구해내지 못합니다.
- 팔레트: 색상별로 이름을 지정하여(
LP_Wall_Light,LP_Roof_Red,LP_Wood_Dark) 재질을 한 번에 만듭니다. - 구조, 그 다음 2차 부품(빔, 프레임, 굴뚝, 계단), 그 다음 작은 디테일(기와, 돌, 판자, 균열), 그 다음 주변(지면 슬래브, 바위 몇 개, 잔디 덤불).
- 거칠게 하기: 규칙 7의 무작위 오프셋 및 기울기.
- 조명 및 프레이밍(아래), 그 다음 뷰를 참조의 각도에 맞추고, 캡처하여 부분별로 비교: 실루엣, 비율, 각 부분의 위치, 색상. 수정.
final_check로 마무리, 작업의 마지막 호출: 함수는 아래 "최종 검사"에 있습니다. 렌더링 결과를 직접 보지 않습니다. 렌더링(배경과 분리된 모델, 참조 이미지 파일 이름이 있으면 그것과 비교)을 측정하고 잘못된 부분에 대해PROBLEM:줄을 출력합니다. 수정하고 다시 실행하고(최대 두 번) 그 출력을 보고서에 넣으세요. 직접 측정한 값은 이를 대체하지 않습니다: 모델을 만들거나 변경하는 작업은 이 함수가 실행될 때까지 완료되지 않습니다.
참조에 대해 두세 번의 수정 라운드를 예상하세요. 여전히 다른 점을 말하세요.
작업당 제한된 도구 라운드가 있습니다(blender-modeling의 "단계 예산" 참조): 호출당 두세 개의 관련 부품을 만들고, 마일스톤(블록아웃, 디테일, 최종)에서 캡처하고, 가장 눈에 띄는 부분을 먼저 배치하세요.
모든 레시피의 코드는 references/low-poly-recipes.md에 있습니다: 팔레트, 플랫 쉐이딩, 테이퍼 상자, 박공 지붕, 기와 줄, 창문과 문, 목재 프레임, 바위, 나무, 지형, 분산 및 조명 설정. 첫 번째 스크립트 전에 열어보세요.
하나의 오브젝트에 여러 블록
돌 층, 흉벽, 포장, 판자, 벽돌은 하나의 오브젝트에 속하는 많은 작은 상자입니다. lp_blocks로 만드세요: 각 상자를 자체 bmesh에서 완성한 다음에야 오브젝트에 추가합니다. 정점 및 면 목록을 수동으로 추가하여 상자를 결합하지 말고, 개수를 변경하는 작업(모따기) 후에 "마지막 N개 정점"을 이동하지 마세요: 면이 잘못된 정점에 배치되어 모델이 늘어난 스파이크처럼 나옵니다. final_check는 그렇게 깨진 오브젝트의 이름을 지정합니다. 다시 만들고, 보고하지 마세요.
import bpy, bmesh
from mathutils import Matrix, Vector
def lp_blocks(name, col, mat, blocks, chamfer=0.03):
"""하나의 플랫 쉐이딩된 오브젝트에 여러 상자를 만듭니다. blocks는 (center, size) 또는
(center, size, z_rotation) 목록입니다: center와 size는 미터 단위의 (x, y, z)이고,
회전은 라디안입니다. 각 상자는 자체 bmesh에서 완성된 후에만 오브젝트에 추가됩니다."""
whole = bmesh.new()
for center, size, *rot in blocks:
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=Vector(size), verts=bm.verts)
if chamfer > 0:
bmesh.ops.bevel(bm, geom=bm.edges[:], offset=min(chamfer, min(size) * 0.25), segments=1, affect='EDGES')
bmesh.ops.transform(bm, matrix=Matrix.Translation(Vector(center)) @ Matrix.Rotation(rot[0] if rot else 0.0, 4, 'Z'), verts=bm.verts)
piece = bpy.data.meshes.new("_piece")
bm.to_mesh(piece)
bm.free()
whole.from_mesh(piece) # 완성된 상자를 오브젝트의 메시에 추가
bpy.data.meshes.remove(piece)
mesh = bpy.data.meshes.new(name)
whole.to_mesh(mesh)
whole.free()
for poly in mesh.polygons:
poly.use_smooth = False
obj = bpy.data.objects.get(name)
if obj is None:
obj = bpy.data.objects.new(name, mesh)
col.objects.link(obj)
else:
obj.data = mesh
obj.data.materials.append(mat)
return obj
# 하나의 오브젝트에 두 가지 크기를 번갈아 가며 배열한 돌 벽 코스 한 줄.
col = bpy.data.collections["LP_Model"]
stone = bpy.data.materials["LP_Stone_Light"]
lp_blocks("Wall_Course_01", col, stone,
[((x * 0.42 - 1.26, -1.0, 0.35), (0.40 if x % 2 else 0.36, 0.20, 0.30)) for x in range(7)])
형태와 그 생성 방법
| 원하는 형태 | 만드는 방법 |
|---|---|
| 벽, 빔, 판자, 계단 | 상자, 크기 조절; 위나 아래를 가늘게 하여 개성 부여 |
| 돌 블록, 흉벽, 포장, 벽돌 | lp_blocks (위): 하나의 오브젝트에 여러 모따기된 상자 |
| 박공 지붕 | 프리즘(삼각형 프로파일을 돌출), 모든 면에서 벽 위로 돌출 |
| 지붕 기와 | 지붕 경사면에 얇은 슬래브 줄, 각 줄이 아래 줄과 겹치고 작은 무작위 크기와 기울기 |
| 굴뚝, 탑, 기둥 | 테이퍼 상자; 뚜껑 슬래브는 약간 더 넓게 |
| 나무 | 줄기: 5-6면 테이퍼 실린더. 잎: 2-3개의 쌓인 원뿔(소나무) 또는 1-3개의 변위된 아이코스피어(활엽수) |
| 바위 | 1단계 세분화된 아이코스피어, 정점을 무작위로 이동, Z축으로 압축, 땅에 파묻음 |
| 지형, 섬 | 절단이 적은 그리드, 노이즈로 정점 높이기, 플랫 쉐이딩; 또는 디오라마 베이스용 두꺼운 불규칙 슬래브 |
| 물 | 평평한 평면 또는 슬래브, 하나의 파란색, 약간 광택(Roughness 0.2) |
| 잔디, 덤불 | 작은 원뿔 또는 압축된 아이코스피어를 클러스터로 |
| 둥근 것(통, 바퀴, 우물) | 6-8면 실린더 |
예산
대략적인 삼각형 개수, 과도하게 제작되었는지 판단하기 위한 것:
| 에셋 | 삼각형 개수 |
|---|---|
| 작은 소품(바위, 상자, 덤불) | 20-150 |
| 나무 | 60-300 |
| 집 또는 건물 | 500-3,000 |
| 캐릭터 | 300-1,500 |
| 전체 디오라마 | 3,000-15,000 |
완료 시 개수를 출력하세요. 예산을 크게 초과하면 일반적으로 너무 많은 세그먼트 또는 세분화하는 모디파이어가 있다는 의미입니다.
조명 및 프레젠테이션
스타일은 조명에 따라 달라집니다: 인접한 면이 다른 밝기를 가질 때만 면이 읽힙니다.
- 하나의 태양을 비스듬히(약 45° 위, 측면으로 30-45°), 강도 3-4, 부드러운 그림자와 함께; 그리고 밝은 하늘색의 월드를 강도 0.6-1.0으로 추가하여 그림자가 완전히 검지 않도록 합니다. 3점 스튜디오 설정은 사용하지 마세요. 흰색 배경을 얻기 위해 빛을 올리지 말고, 색상을 입히기 위해 월드에 틴트를 주지 마세요: 배경판의 자체 재질이 색상을 제공합니다.
- 약간 따뜻한 태양과 약간 차가운 월드는 고전적인 룩을 제공합니다.
- 뷰 변환을
Standard로 설정하여 팔레트 색상이 선택한 대로 유지되도록 하세요(기본 변환은 색상을 회색으로 만듭니다). - 카메라: 위에서 3/4 뷰(아래로 30-35°), 긴 렌즈(50-85 mm) 또는 디오라마용 직교 투영.
- 팔레트 색상의 평범한 배경, 또는 깨끗한 가장자리로 끝나는 지면 슬래브. 배경 평면은 모델보다 훨씬 커야 하거나(20배 이상), 월드가 같은 색상이어야 합니다: 프레임에 가장자리나 지평선이 보여서는 안 됩니다.
- EEVEE로 충분하고 빠릅니다. 부드러운 그림자와 앰비언트 오클루전이 많은 도움이 됩니다.
재질, 파일로 렌더링, 문제 해결은 blender-materials-render에 있습니다.
게임 엔진용
로우폴리 에셋은 일반적으로 엔진으로 이동합니다. 내보낼 때 플랫 쉐이딩을 유지하고(면은 분할 법선으로 저장되어 정점 개수는 증가하지만 삼각형 개수는 증가하지 않음), 하나의 에셋 조각을 몇 개의 재질 슬롯이 있는 하나의 메시로 병합하고, 원점을 하단 중앙에 배치하세요. 나머지는 blender-game-export를 따르세요.
최종 검사 (항상 마지막 호출)
렌더 도구는 파일을 쓰고 그 경로를 제공합니다: 이미지를 직접 보지 않습니다. 따라서 작업의 마지막 execute_blender_code 호출은 final_check이며, 그대로 복사되고, 출력하는 내용이 그대로 보고서에 들어갑니다. 장면의 카메라에서 작은 렌더를 만들고 잘못된 것을 지적합니다: 재질이 없는 오브젝트, 깨진 지오메트리, 프레임 밖의 모델, 타거나 어두운 렌더, 그리고 참조 파일 이름이 있으면 프레이밍, 밝기, 색상 및 실루엣의 단단함 차이.
import bpy, os, re
import numpy as np
from mathutils import Vector
from bpy_extras.object_utils import world_to_camera_view
BACKDROP_WORDS = ("ground", "backdrop", "floor", "sky", "shadow")
def subject_stats(path, size=128):
"""이미지를 배경(네 모서리의 색상)과 피사체(나머지)로 분할:
이미지에서 피사체가 차지하는 비율, 경계 상자, 밝기, 채도 및 색상."""
img = bpy.data.images.load(path, check_existing=False)
w, h = img.size
sw, sh = (size, max(8, round(size * h / w))) if w >= h else (max(8, round(size * w / h)), size)
img.scale(sw, sh)
px = np.empty(len(img.pixels), dtype=np.float32)
img.pixels.foreach_get(px)
bpy.data.images.remove(img)
rgb = px.reshape(sh, sw, 4)[:, :, :3] # 행 0은 이미지의 맨 아래
cy, cx = max(2, sh // 8), max(2, sw // 8)
corners = np.concatenate([rgb[:cy, :cx].reshape(-1, 3), rgb[:cy, -cx:].reshape(-1, 3),
rgb[-cy:, :cx].reshape(-1, 3), rgb[-cy:, -cx:].reshape(-1, 3)])
# 배경 = 네 모서리 색상 중 하나에 가까움(그라데이션이나 부드러운 그림자도 포함).
q = np.unique(np.round(corners * 8).astype(int), axis=0)
flat = rgb.reshape(-1, 3)
dist = np.min(np.abs(flat[:, None, :] * 8 - q[None, :, :]).max(axis=2), axis=1)
subject = (dist > 1.0).reshape(sh, sw)
share = float(subject.mean())
whole = flat @ np.array([0.2126, 0.7152, 0.0722], dtype=np.float32)
out = {"size": (w, h), "image_brightness": round(float(whole.mean()), 3), "subject_pct": round(100 * share, 1)}
if share < 0.005:
return
…
(번역을 위해 잘렸습니다)