【Blender】客製化建立Shape Keys

帽捲
Maochinn
Published in
22 min readJul 18, 2024

本篇將介紹如何利用腳本在Blender中快速定義Shape,並且建立成不同的Shape Keys,簡單來說,Shape Keys能紀錄Mesh各種不同的形狀而不破壞最原始的佈線,具體來說,就是Mesh中同一個vertex可以有不同的position。

實際應用上,在Computer Animation中,想像一個最粗暴的做法去儲存每一幀的所有場景,包含燈光、相機以及Mesh,就類似傳統2D動畫的邏輯,換言之,假設一個Mesh有1MB,那麼一分鐘60FPS的動畫就需要60*60*1MB = 3.6G,顯然,這並不是一個合理的作法。

但是,就像2D動畫有所謂的Key Frame(關鍵幀)一樣,3D動畫也有類似的作法,也就是指儲存關鍵幀的Mesh一樣,其餘的幀就使用內插來計算,其中,每一個關鍵的Mesh可以稱為Shape,因此,這樣的技術稱之為Shape Keys。

除了省記憶體的優點之外,這樣的方式也讓人可以更容易的操作,例如,人臉的動畫就是最常見的例子,因為我們不大可能去窮舉出所有人的表情變化,但是我們可以定義出喜、怒、哀、樂等表情的Shape後,在自由的依照各種比例去組合出不同表情,或者我們更擴展開來說,Live2D也是類似的概念。

https://graphicalanomaly.wordpress.com/2015/07/17/blendshapes/

根據Wiki的說明,這樣的技術也稱之為Morph target animation, per-vertex animation, shape interpolation, shape keys, or blend shapes,從字面上,大概可以理解我們就是per-vertex的去定義每個shape keys,然後再用interpolation去blend出結果。

事實上,Blender就是使用Shape Keys來表示這個功能,如果從【Blender】客製化建立Attributes的角度來看,你也可以認為Shape就是另一組Attribute,只是這組Attribute儲存的是vertex position,同時,因為每個Shape的vertex通常都是一一對應的,因此完全可以把它當作Attribute,然後自己寫Shader來做內插。

當然,如果Blender中已經實現了這些細節,當然也就沒有必要自己實作,何況這項技術基本上在各大軟體也都有對應,例如Unity中的Skinned Mesh。

總之,我們可以先來影片中看看簡單的操作

簡單來說,就是可以生成多個Shape,然後定義好各自的vertices,接著就可以透過0~1之類的數值去控制形狀,但是,如果你的shape是在腳本中定義的呢?不可能人工把每個vertex移動到對應的位置來達成。

因此,這邊延續【Blender】客製化建立簡單Mesh的Grid來舉例,生成完Mesh後,可以在Properties/Data/ShapeKeys中點擊+號,增加一個Basis的Shape。

這就建立了一個基準的Shape,接著我們要生成另一個Shape才能夠混和兩者,首先執行下面的腳本

import bpy

list = []
list.append((0.0000, 0.0000))
list.append((45.0000, 0.0000))
list.append((90.0000, 0.0000))
list.append((135.0000, 0.0000))
list.append((180.0000, 0.0000))
list.append((225.0000, 0.0000))
list.append((270.0000, 0.0000))
list.append((315.0000, 0.0000))
list.append((360.0000, 0.0000))
list.append((405.0000, 0.0000))
list.append((450.0000, 0.0000))
list.append((495.0000, 0.0000))
list.append((540.0000, 0.0000))
list.append((585.0000, 0.0000))
list.append((630.0000, 0.0000))
list.append((675.0000, 0.0000))
list.append((720.0000, 0.0000))
list.append((765.0000, 0.0000))
list.append((810.0000, 0.0000))
list.append((855.0000, 0.0000))
list.append((900.0000, 0.0000))
list.append((0.0000, 50.0000))
list.append((45.0000, 50.0000))
list.append((90.0000, 50.0000))
list.append((135.0000, 50.0000))
list.append((180.0000, 50.0000))
list.append((225.0000, 50.0000))
list.append((270.0000, 50.0000))
list.append((315.0000, 50.0000))
list.append((360.0000, 50.0000))
list.append((405.0000, 50.0000))
list.append((450.0000, 50.0000))
list.append((495.0000, 50.0000))
list.append((540.0000, 50.0000))
list.append((585.0000, 50.0000))
list.append((630.0000, 50.0000))
list.append((675.0000, 50.0000))
list.append((720.0000, 50.0000))
list.append((765.0000, 50.0000))
list.append((810.0000, 50.0000))
list.append((855.0000, 50.0000))
list.append((900.0000, 50.0000))
list.append((0.0000, 100.0000))
list.append((45.0000, 100.0000))
list.append((90.0000, 100.0000))
list.append((135.0000, 100.0000))
list.append((180.0000, 100.0000))
list.append((225.0000, 100.0000))
list.append((270.0000, 100.0000))
list.append((315.0000, 100.0000))
list.append((360.0000, 100.0000))
list.append((405.0000, 100.0000))
list.append((450.0000, 100.0000))
list.append((495.0000, 100.0000))
list.append((540.0000, 100.0000))
list.append((585.0000, 100.0000))
list.append((630.0000, 100.0000))
list.append((675.0000, 100.0000))
list.append((720.0000, 100.0000))
list.append((765.0000, 100.0000))
list.append((810.0000, 100.0000))
list.append((855.0000, 100.0000))
list.append((900.0000, 100.0000))
list.append((0.0000, 150.0000))
list.append((45.0000, 150.0000))
list.append((90.0000, 150.0000))
list.append((135.0000, 150.0000))
list.append((180.0000, 150.0000))
list.append((225.0000, 150.0000))
list.append((270.0000, 150.0000))
list.append((315.0000, 150.0000))
list.append((360.0000, 150.0000))
list.append((405.0000, 150.0000))
list.append((450.0000, 150.0000))
list.append((495.0000, 150.0000))
list.append((540.0000, 150.0000))
list.append((585.0000, 150.0000))
list.append((630.0000, 150.0000))
list.append((675.0000, 150.0000))
list.append((720.0000, 150.0000))
list.append((765.0000, 150.0000))
list.append((810.0000, 150.0000))
list.append((855.0000, 150.0000))
list.append((900.0000, 150.0000))
list.append((0.0000, 200.0000))
list.append((45.0000, 200.0000))
list.append((90.0000, 200.0000))
list.append((135.0000, 200.0000))
list.append((180.0000, 200.0000))
list.append((225.0000, 200.0000))
list.append((270.0000, 200.0000))
list.append((315.0000, 200.0000))
list.append((360.0000, 200.0000))
list.append((405.0000, 200.0000))
list.append((450.0000, 200.0000))
list.append((495.0000, 200.0000))
list.append((540.0000, 200.0000))
list.append((585.0000, 200.0000))
list.append((630.0000, 200.0000))
list.append((675.0000, 200.0000))
list.append((720.0000, 200.0000))
list.append((765.0000, 200.0000))
list.append((810.0000, 200.0000))
list.append((855.0000, 200.0000))
list.append((900.0000, 200.0000))
list.append((0.0000, 250.0000))
list.append((45.0000, 250.0000))
list.append((90.0000, 250.0000))
list.append((135.0000, 250.0000))
list.append((180.0000, 250.0000))
list.append((225.0000, 250.0000))
list.append((270.0000, 250.0000))
list.append((315.0000, 250.0000))
list.append((360.0000, 250.0000))
list.append((405.0000, 250.0000))
list.append((450.0000, 250.0000))
list.append((495.0000, 250.0000))
list.append((540.0000, 250.0000))
list.append((585.0000, 250.0000))
list.append((630.0000, 250.0000))
list.append((675.0000, 250.0000))
list.append((720.0000, 250.0000))
list.append((765.0000, 250.0000))
list.append((810.0000, 250.0000))
list.append((855.0000, 250.0000))
list.append((900.0000, 250.0000))
list.append((0.0000, 300.0000))
list.append((45.0000, 300.0000))
list.append((90.0000, 300.0000))
list.append((135.0000, 300.0000))
list.append((180.0000, 300.0000))
list.append((225.0000, 300.0000))
list.append((270.0000, 300.0000))
list.append((315.0000, 300.0000))
list.append((360.0000, 300.0000))
list.append((405.0000, 300.0000))
list.append((450.0000, 300.0000))
list.append((495.0000, 300.0000))
list.append((540.0000, 300.0000))
list.append((585.0000, 300.0000))
list.append((630.0000, 300.0000))
list.append((675.0000, 300.0000))
list.append((720.0000, 300.0000))
list.append((765.0000, 300.0000))
list.append((810.0000, 300.0000))
list.append((855.0000, 300.0000))
list.append((900.0000, 300.0000))
list.append((0.0000, 350.0000))
list.append((45.0000, 350.0000))
list.append((90.0000, 350.0000))
list.append((135.0000, 350.0000))
list.append((180.0000, 350.0000))
list.append((225.0000, 350.0000))
list.append((270.0000, 350.0000))
list.append((315.0000, 350.0000))
list.append((360.0000, 350.0000))
list.append((405.0000, 350.0000))
list.append((450.0000, 350.0000))
list.append((495.0000, 350.0000))
list.append((540.0000, 350.0000))
list.append((585.0000, 350.0000))
list.append((630.0000, 350.0000))
list.append((675.0000, 350.0000))
list.append((720.0000, 350.0000))
list.append((765.0000, 350.0000))
list.append((810.0000, 350.0000))
list.append((855.0000, 350.0000))
list.append((900.0000, 350.0000))
list.append((0.0000, 400.0000))
list.append((45.0000, 400.0000))
list.append((90.0000, 400.0000))
list.append((135.0000, 400.0000))
list.append((180.0000, 400.0000))
list.append((225.0000, 400.0000))
list.append((270.0000, 400.0000))
list.append((315.0000, 400.0000))
list.append((360.0000, 400.0000))
list.append((405.0000, 400.0000))
list.append((450.0000, 400.0000))
list.append((495.0000, 400.0000))
list.append((540.0000, 400.0000))
list.append((585.0000, 400.0000))
list.append((630.0000, 400.0000))
list.append((675.0000, 400.0000))
list.append((720.0000, 400.0000))
list.append((765.0000, 400.0000))
list.append((810.0000, 400.0000))
list.append((855.0000, 400.0000))
list.append((900.0000, 400.0000))

mesh = bpy.data.objects['Grid']

N = len(mesh.data.vertices.values())
for i in range(0, N):
vertex = mesh.data.vertices[i]

vertex.co.x = 900 - list[i][0]
vertex.co.y = list[i][1]

可以注意到,跟之前的腳本唯一的差異是

vertex.co.x = 900 - list[i][0]

簡單來說就是我做了一個X方向的反轉(0 ←→ 900),因此乍看之下你的Grid沒有改變,此時再增加一個Shape(Key 1),並且調整Value就可以在viewport中看到兩者混合的結果。

這邊要注意一個點,要先執行腳本再新增shape,這個順序要對,事實上,只要Shape Keys有一個Shape時,你去改動mesh.data.vertices中任何值都不會反映到畫面上,或者說,腳本執行下去後場景中的Mesh無論如何都不會有任何變化,但如果不去深究原因的話,單純照著這個順序操作就行,基本上本篇就結束了。

但是如果你對於細節有興趣,可以打開Data API可以看到Shape Keys中有兩個Key Blocks,其實就是兩個shape,它們分別都有189個item,聰明的你也可以猜到每個Key Blocks都儲存一組vertices就像是mesh.data.vertices,事實上也是如此,而更進一步的過程,會在後面解釋。

首先,個人的猜測是,開啟Shape Keys之後,場景中每個vertex的位置都是透過Shape Keys計算出來的,此時畫面上Mesh的Shape,或者說每個vertex的position並不是來自於mesh.data.vertices,而是由某幾個Shape混和而來,因此就算你去改動mesh.data.vertices也影響不了,但是為甚麼上述的操作仍會成立,我們可以直接trace Blender的code。

首先,我們可以從Blender得知,+號,也就是增加一個Shape Key對應的function是shape_key_add,同時參照官方文件

https://docs.blender.org/api/current/bpy.types.Object.html

可以知道,有一個參數是from_mix,也就是增加一個shape時,是否要從現有多個shapes中混和內插出一個shape,那接下來的問題是,因為預設是否,那會發生甚麼事,這個shape要怎麼得到?

接著,打開Blender的Github,直接搜尋shape_key_add

blender/source/blender/editors/object
/object_shapekey.cc

可以看到這個function會將from_mix傳入BKE_object_shape_insert

(補充:BKE=Blender Kernel)

blender/source/blender/blenkernel/intern
/object.cc

接著,目前我們只關心Mesh的操作,所以進一步看insert_meshkey

blender/source/blender/blenkernel/intern
/object.cc

到這邊大致就可以猜測到事發生甚麼事情了,當from_mix==false,時會複製一份mesh的vertices去定義新增的Key Blocks,或者說shape,這邊細節我就不展開,有興趣的自己往下trace。

blender/source/blender/blenkernel/intern
/key.cc

如果from_mix==true,則就會計算一個shape從現有的shape,這邊我只節錄一小段,就是初始化新增的shape的memory,這邊一樣我就不展開了。

總而言之,大概可以理解為,在Shape Keys中按下+號就會呼叫

shape_key_add(from_mix = false)

然後就會去複製mesh.data.vertices中的值給新的Key Blocks,或者說是Shape,這樣就可以理解為何一定要先執行腳本再新增shape,因為你需要先執行腳本把值塞進去mesh.data.vertices,然後按下+號就會把這些vertices複製給新的Shape,只是比較tricky的點是,當你修改mesh.data.vertices時畫面不會有相應的變化,只能透過Data API或是console確保你有把值改掉。

另外,如果你在edit mode去改動mesh.data.vertices,再切回object mode你會發現mesh.data.vertices的值會被改回來,或者更準確的說,被Shape Keys改回來。

--

--