Blender中的低多边形建模
低多边形是一种风格,而不仅仅是低三角形数量:大的可读形状、可见的平直刻面、少量纯色以及略高于现实的比例。一个多边形较少但写实的模型不是低多边形;它是一个粗糙的写实模型。
一般的工作循环(查看场景、规划部件、每次调用构建一个部件、每一步后截图、绝不删除你未创建的内容)在 blender-modeling 技能中:请遵循它。本技能说明低多边形部分的不同之处。
你的Blender连接
你通过 BlenderLab(官方Blender实验室服务器)工作;其工具为 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配合area_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边的圆柱体比任何其他东西都更破坏外观。
- 无倒角,无细分曲面。 它们会使刻面变圆。对于更柔和的边缘,最多使用一段倒角。
- 纯色,无纹理。 每种颜色一个材质,Principled BSDF 使用基色、粗糙度0.8-1.0、金属度0,无镜面光泽。重复使用材质:整个场景通常需要6-12个。
- 有意的调色板。 在建模前挑选:1-2种主色,2-3种辅色,1种强调色。略为去饱和的中间调,冷暖平衡,每种主色有两种色调(亮和暗)以在相邻部件之间产生变化。
- 粗犷、夸张的比例。 墙壁、横梁和树干比现实更粗;过大的屋顶、门和窗户;锥形形状(底部较宽,或顶部较宽以营造卡通感)。在这种细节级别下,细小的部分会消失。
- 少量手动的不完美。 没有完美对齐:将烟囱倾斜2-4°,将屋顶瓦片的大小变化百分之几且倾斜2-3°,随机移动岩石和地形的顶点。结构必须仍然可读:成排的瓦片保持成排,没有碎片侧翻或脱离表面。使用固定的随机种子,以便重新运行得到相同结果。
- 通过分离的小块添加细节,而非通过细分。 窗户是由四个盒子加一个窗格组成的框架;屋顶瓦片是小板条的行;石头是嵌入墙壁的一些不规则块。每个部分简单;排列提供了丰富性。
- 部件可以相互穿插。 低多边形部件被推入彼此而不是焊接:横梁陷入墙壁,岩石陷入地面。不要在布尔运算或干净接合上花费精力。
工作流程
- 如果有参考,请阅读:列出部件,统计重复的物体(排瓦片、横梁数量、每面墙的窗户),将部件之间的比例记为比值(屋顶高度与墙高之比、宽度与高度之比),并命名调色板颜色。据此列表构建。图像决定:锚定一个尺寸,从比值推导出其余尺寸,不要发明图片矛盾的以米为单位的尺寸,不要添加图片未显示的内容(底座、道具),除非被要求。
- 遮挡 首先用盒子和楔形块以正确的比例构建大的体量。截图并比较轮廓:如果轮廓错误,细节也无法补救。
- 调色板:一次性创建材质,以它们着色的内容命名(
LP_Wall_Light、LP_Roof_Red、LP_Wood_Dark)。 - 结构,然后是 次要部件(横梁、框架、烟囱、台阶),接着是 小细节(瓦片、石头、木板、裂缝),然后是 周围环境(地面板、几块岩石、草丛)。
- 粗糙化:规则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 是一个 (中心, 尺寸) 或 (中心, 尺寸, z轴旋转) 的列表:
中心和尺寸以米为单位 (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轴方向压扁,沉入地面 |
| 地形、岛屿 | 少量切分的网格,通过噪点提升顶点,然后平直着色;或用于微缩景观底座的厚不规则板 |
| 水面 | 平面或平板,一个蓝色,略带光泽(粗糙度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,使阴影不完全是黑色。不采用三点式工作室布光。不要为了得到白色背景而提高灯光,也不要给世界着色:背景的材质提供颜色。
- 略暖的太阳配上略冷的世界,可产生经典外观。
- 将视图变换设置为
Standard,使调色板颜色保持不变(默认变换会使它们变灰)。 - 相机:从上方四分之三视角(向下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 None
…
[为翻译而截断]