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,配合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是一個列表,元素為(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軸上壓扁,沉入地面 |
| 地形、島嶼 | 具有少量切割的網格,頂點通過噪聲升高,然後平面著色;或用於微縮場景底座的厚不規則板塊 |
| 水 | 一個平面或薄板,藍色,略微有光澤(粗糙度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
[後續程式碼省略以進行翻譯]