2021年4月27日 星期二

補強 Blender 的 Shader node

 補強 Blender 的 Shader node

前言

  Blender 的 Shader node 提供的預設節點(node)種類有點少,少到有時一些基本的功能也必須用組合的,這裡開一篇來記錄相關的應用。


內容

Switch color:

  Shader node 並沒有提供 switch node ,但如果需要時該如何做呢?如下圖

switch node 的替代方案

範例的結果為當"Value"小於等於1時會是"紅色",大於且小於等於2時會是"綠色",大於2會是"藍色",要注意的是因為用"Greater Than"來完成,所以剛好等於1時還會是"紅色"。


Not equal:

  Shader node 有提供"=="也就是"Compare",但如果需要"!="(Not equal)呢?如下圖

實現"!="(Not equal)

Greater equal or Less equal:

  Shader node 僅提供"Greater Than"和"Less Than",如下圖

實現">="(Greater equal)

實現"<="(Less equal)

Not:

  Not 的運算是跟輸入相反,如下圖
實現"!"(Not)


Not 的運算必須考慮輸入型態是 float ,所以要先確認是否為"0",接著就可以去確定是否為"1",第二次的檢查可以確定輸入不是"0"就是"1",結果就會產生只有在輸入是"0"時結果為"1"。

And:

  And 的實現如下
實現"&"(And)

每個輸入都要經過兩次"Compare",目的是要將輸入是"0"時輸出"0",輸入是非零值時輸出"1",最後經過"Multiply"即為結果。


Or:

  Or 的實現如下
實現"|"(Or)

與 And 類似,但最後的運算改成"Add",並記得需要"Clamp",因為當輸入都是"1"時,輸出必須還是"1"。

Nand:

  Nand 的實現如下
實現 Nand

在 And 的結尾加上 Not 即為所求。


Nor:

  Nor 的實現如下
實現 Nor

在 Or 的結尾加上 Not 即為所求。

Xor:

  Xor 的實現如下
實現 Xor

將兩個輸入拿來"Compare"後再過 Not 即為所求。

Xnor:

  Xnor 的實現如下
實現 Xnor

移除 Xor 最後的 Not 即為所求。

2021年4月20日 星期二

在 Blender 匯入 Blend 檔裡的資料

在 Blender 匯入 Blend 檔裡的資料

前言

  由於 Blender 2.8 以後改採 Shader node 的方式來調整材質(Material),材質(Material)的內容可能變得相當複雜,要複製一個材質(Material)總不能從新編輯一個一樣的,能匯入嗎?這是很自然的想法,答案是可以的,在此把學習的過程做個紀錄。


內容

  在開始解說如何匯入前先來看到 Blender 是如何儲存資料的,如下圖操作

顯示 Blend 檔的結構

接著如下圖

Blend 檔的結構


可以在上圖發現 Blend 檔儲存相當多的資料,如 Mesh 、 Material ... Object 等,這些並不是全部的種類,有些種類會在有的時候才儲存,如 Shader node,上圖不過是預設的場景(Scene)所儲存的內容。


  在了解到 Blender 檔有儲存那些資料後接著就是如何匯入,如下圖操作

操作匯入 Blender 檔的資料

接著選擇要從哪個 Blender 檔匯入,如下圖
在要匯入的 Blender 檔上雙擊滑鼠

這個操作介面有個特別之處,在 Blender 檔上雙擊滑鼠並非選取該檔,而是把該檔像是資料夾一樣打開,範例使用 myMaterialDepot.blend 是一個示範用的檔案,並不具有特別意義。雙擊後可以看到以下
欲匯入的 Blend 檔的資料結構


這個會跟在觀看 Blend 檔的視窗顯示一樣的結構,接著選取要匯入的資料(Material、Mesh ... Object 等)用雙擊滑鼠或用右下的"Apend"按鈕就可以完成匯入。

2021年4月18日 星期日

最近學的 Python 新寫法

 最近學的 Python  新寫法

前言

  由於 Python 並非我擅長的語言,但因為工作環境時常會需要一些簡短的 Python 程式碼,如 Blender 的 Script ,所以寫法一直沒有進步,容易寫出冗長的程式碼,最近抽空學習了 Python 的新寫法,在此做個紀錄。


內容

瀏覽 List :

ar = [ 'one', 'two', 'three']
#Old style loop
for ele in range( len( ar ) ):
    print(ar[ ele ])
#New style loop1
for ele in ar:
    print(ele)
#New style loop2
for i, ele in enumerate( ar ):
    print( 'i:', i, ' ele:', ele)


舊的寫法很冗長,不但要 range() 還要 len() 搭配才能瀏覽陣列的內容,新的寫法簡單的多,如果有需要 index 變數可以搭配 enumerate() 來瀏覽。


瀏覽 Dictionary :

my_map = {
    'a' : 1,
    'b' : 2,
    'c' : 3
}
#Old style
for key in my_map:
    print( 'key:', key , ' value:', my_map[ key ] )
#New style1
for key, value in my_map.items():
    print( 'key:', key, ' value:', value)
#New style2
for value in my_map.values():
    print( value )


舊的寫法可以視為只取 key 值,然後再從 key 取得 value ,取得 value 時會較冗長,新的寫法透過 Dictionary 的 items() ,如果很確定只需要取 value 的話可以透 Dictionary 的 values() 來取得。


字串:

a = 'string'
b = 1234
#Old style
print( 'a:', a, "b:", b)
#New style
print( f'a:{a} b:{b}' )


舊的寫法會需要很多的',',新的寫法在字串的前方加'f',在字串裡可以透過"{}"來取得變數,整體看起來非常直覺與簡短。

 

參考資料

[ Mr. Opengate ] Pythonic 實踐:實用的 python 慣用法整理

2021年4月6日 星期二

在 Blender 開發 Addon 時載入自己的 Module

 在 Blender 開發 Addon 時載入自己的 Module

前言

  由於需要從 Blender 匯出自製遊戲引擎的格式,所以就需要開發 Addon ,但由於要支援的規格越來越多,所以程式碼就越來越多,全部都塞在一個檔案寫起來並不是很舒服,之前不拆成其它 Module 來載入是因為找不到方法來載入,官方並沒有提供載入 Module 的範例,但最近在 Valve 的 Source 引擎提供給 Blender 的 Addon 裡發現它可以載入自己的 Module ,所以就來研究是如何做的,在此做個紀錄。


內容

  Valve 的 Source 引擎提供給 Blender 的 Addon 可以在 [ steamreview.org ] Blender Source Tools 下載。在研究中發現以前從沒想過把 Addon 裝起來跟直接在 Blender 的文字介面有何不同,所以總是在文字介面裡做實驗,所以 Import 自己的 Module 總是會失敗,但我看了 Valve 提供的 Addon ,裡面遽然可以直接 Import 自己的 Module ,本以為事情解決了,但發現一些小毛病!如果在安裝的環境下開發,當程式被修改時, Blender 並不會去 Reload 它,這裡我找到兩種方法來 Reload ,一種是直接用 Blender 的操作介面來 Reload ,如下圖

Reload Addon 在 Blender 的介面

這個方法很簡潔,但不知為什麼每次 Reload 時會小小的頓一下,所以我個人比較建議第二種方法。第二種方法是利用 Addon 的啟動按鍵來 Reload ,如下圖

利用 Addon 的起用來 Reload

這個方法有個麻煩, 只是單純的利用關閉後再啟用會發現其實它不會被 Relaod ,但如果主程式(__init__.py)的修改日期有變動時,它就會 Reload,這種 Reload 操作會負責一點,但可以確定只 Reload 這個 Addon。解決了不會 Reload 後又發現當自己的 Module 被修改時,也不會 Reload! Reload 似乎只針對主程式(__init__.py),所以需要在主程式增加 Reload 自己的 Module ,這段程式碼我在 Valve 的 Addon 裡找到了如下


 # Python doesn't reload package sub-modules at the same time as __init__.py!
import importlib, sys
for filename in [ f for f in os.listdir(os.path.dirname(os.path.realpath(__file__))) if f.endswith(".py") ]:
	if filename == os.path.basename(__file__): continue
	module = sys.modules.get("{}.{}".format(__name__,filename[:-3]))
	if module: importlib.reload(module)

# clear out any scene update funcs hanging around, e.g. after a script reload
from bpy.app.handlers import depsgraph_update_pre, depsgraph_update_post
for func in depsgraph_update_post:
	if func.__module__.startswith(__name__):
		depsgraph_update_post.remove(func)
 

要在主程式裡加這段才可以 Reload 自己的 Module 。完整的範例程式碼如下

__init__.py

bl_info = {
	"name": "My export addon",
	"author": "Hosee",
	"version": (1, 0, 0),
	"blender": (2, 80, 0),
	"category": "Import-Export",
	"location": "",
	"wiki_url": "",
	"tracker_url": "",
	"description": "My custom export addon."
}
#
import bpy, os
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator

#follow code from Valve
# Python doesn't reload package sub-modules at the same time as __init__.py!
import importlib, sys
for filename in [ f for f in os.listdir(os.path.dirname(os.path.realpath(__file__))) if f.endswith(".py") ]:
	if filename == os.path.basename(__file__): continue
	module = sys.modules.get("{}.{}".format(__name__,filename[:-3]))
	if module: importlib.reload(module)

# clear out any scene update funcs hanging around, e.g. after a script reload
from bpy.app.handlers import depsgraph_update_pre, depsgraph_update_post
for func in depsgraph_update_post:
	if func.__module__.startswith(__name__):
		depsgraph_update_post.remove(func)
#
#import my module
from . import utility
#
class ExportSomeData(Operator, ExportHelper):
	bl_idname = "export_test.something" 
	bl_label = "Export something"

	filename_ext = ".txt"

	filter_glob: StringProperty(
		default="*.txt",
		options={'HIDDEN'},
		maxlen=255,  # Max internal buffer length, longer would be clamped.
	)

	use_setting: BoolProperty(
		name="Example Boolean",
		description="Example Tooltip",
		default=True,
	)

	type: EnumProperty(
		name="Example Enum",
		description="Choose between two items",
		items=(
			('OPT_A', "First Option", "Description one"),
			('OPT_B', "Second Option", "Description two"),
		),
		default='OPT_A',
	)

	def execute(self, context):
		utility.do_something()
		return {"FINISHED"}


# Only needed if you want to add into a dynamic menu
def menu_func_export(self, context):
	self.layout.operator(ExportSomeData.bl_idname, text="Text Export Operator")


def register():
	bpy.utils.register_class(ExportSomeData)
	bpy.types.TOPBAR_MT_file_export.append(menu_func_export)


def unregister():
	bpy.utils.unregister_class(ExportSomeData)
	bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)

if __name__ == "__main__":
		register()


utility.py

def do_something():
	print('Do something')


完整的程式碼可以在 [ Gitlab ] basic_export_addon 下載。

 

  雖然照著上述的做法就可以載入自己的 Module ,但還是有些麻煩,如修改後要測試要經過 Reload 才能看到結果,或是修改後如果要搭配版本管理(Version control)時,要把修改的那一份(Blender 安裝的資料夾)複製到版本管理(Version control)才能 Commit ,這些麻煩目前還找不到方法解決,如果解決了會再寫一篇。


參考資料

[ steamreview.org ] Blender Source Tools


相關資料

[ Gitlab ] basic_export_addon


2021年3月29日 星期一

從Blender裡萃取模組資料(11)

 從Blender裡萃取模組資料(11)

前言

  在之前的 從Blender裡萃取模組資料(9) 裡有示範如何萃取 Texture 的資料,但 Blender 2.8 以後的 Material 採取 Node 編輯的方式設定,所以該範例的方法已無法在  Blender 2.8 以後使用,這次就來研究如何萃取,在此把學習的過程做個紀錄。


內容

  在開始之前請先將 Material 調成以下

新增 Texture 給 Base Color


這次需要示範如何萃取 Image Texture 的資料,所以要新增一張圖並連結到"Base Color"。這次的範例會示範如何萃取連結到"Base Color"接口的 Image Texture,具體的範例如下

import bpy
import bmesh

tagMesh = bpy.data.meshes["Cube"]
#Create bmesh from mesh
tagBMesh = bmesh.new()
tagBMesh.from_mesh(tagMesh)

#
def find_node_by_name( srcNode , nodeName):
    if srcNode.name == nodeName:
        return srcNode
    tagNode = None
    for socket in srcNode.inputs:
        for link in socket.links:
            tagNode = find_node_by_name( link.from_node , nodeName )
            if tagNode != None:
                break
        if tagNode != None:
            break
    #
    return tagNode
#
def find_node_from_node_socket( srcNode , socketName , nodeName):
    tagSocket = srcNode.inputs.get( socketName )
    if tagSocket == None:
        return None
    tagNode = None
    for link in tagSocket.links:
        tagNode = find_node_by_name( link.from_node , nodeName )
        if tagNode != None:
            break
    #
    return tagNode
#Start extract
matList = tagMesh.materials

for mat in matList:
    print( 'material name:' , mat.name )
    outputNode = mat.node_tree.nodes.get( 'Material Output' )
    if outputNode != None:
        principledBSDFNode = find_node_by_name( outputNode , 'Principled BSDF' )
        if principledBSDFNode != None:
            imageNode = find_node_from_node_socket( principledBSDFNode , 'Base Color' , 'Image Texture' )
            if imageNode != None:
                print( 'Image name:' , imageNode.image.name)
                print( 'Image width: ', imageNode.image.size[0] )
                print( 'Image height: ', imageNode.image.size[1] )
                print( 'Image format: ', imageNode.image.file_format )
                print( 'Image path: ' , imageNode.image.filepath_from_user() )
                print( 'Image Node interpolation:' , imageNode.interpolation )
                print( 'Image Node projection:' , imageNode.projection )
                print( 'Image Node extension:' , imageNode.extension )
                print( 'Image source:' , imageNode.image.source )
                print( 'Image color space:' , imageNode.image.colorspace_settings.name )
            else:
                #Show default value
                socket = principledBSDFNode.inputs.get( 'Base Color' )
                for val in socket.default_value:
                    print( 'data:' , val )
  
#Free bmesh...
tagBMesh.free()


範例裡有兩個工具 Function ,分別為"find_node_by_name"與"find_node_from_node_socket",第一個是基於某個 Node 來從所有的輸入接口尋找目標 Node ,第二則是基於某個 Node 的指定接口來尋找目標 Node 。萃取的過程比想像中簡單,透過 Material 裡的"node_tree"來取得輸出的 Node (Material Output),接著透過"find_node_by_name"來尋找"Principled BSDF" Node ,用工具 Function 來尋找是因為不知道中間會過多少個 Node ,接著就透過"find_node_from_node_socket"從"Base Color"接口來尋找"Image Texture",範例在找到"Image Texture"時就取得 Node 的相關資料,當沒取得時就取得"Base Color"的顏色資料(當該接口沒有被連結時)。


  在取得"Base Color"的顏色資料時是透過"default_value",但官方的說明文件上沒有這個屬性,在 [ blender.stackexchange.com ] PyNodes API: transferring data between nodes with sockets 裡我才找到有這個屬性可以提取。這次範例的工具 Function 都只找出第一個目標 Node ,無法解決多個目標 Node 的狀況。由於 Material 採取 Node 編輯,但由於每個遊戲引擎的 Node 規格不太一樣,就算詳細萃取的每個 Node 的資料也很難可以匯出給其它的遊戲引擎,所以只提供這樣不完整的萃取方法,如果這樣不夠用以後在補新的。

 

參考資料

[ docs.blender.org ] Blender 2.92.0 Python API Documentation

[ blender.stackexchange.com ] PyNodes API: transferring data between nodes with sockets


相關文章

從Blender裡萃取模組資料(9)

2021年3月23日 星期二

從Blender裡萃取模組資料(10)

 從Blender裡萃取模組資料(10)

前言

  在先前的"從Blender裡萃取模組資料"系列文章中發現在萃取 Material 的時候沒有萃取對應的 Vertex ,也就是一個 Mesh 有多個 Material 時要如何萃取,在此做個紀錄。


內容

  先看以下範例

import bpy
import bmesh

tagMesh = bpy.data.meshes["Cube"]
#Create bmesh from mesh
tagBMesh = bmesh.new()
tagBMesh.from_mesh(tagMesh)

#Start extract
matList = tagMesh.materials

for face in tagBMesh.faces:
    #Start extract face data
    print( 'material index:' , face.material_index )
    print( 'material name:' , matList[ face.material_index ].name)
  
#Free bmesh...
tagBMesh.free()


要萃取 Vertex 所屬的Material 可以透過 bmesh 來取得,加上先前 從Blender裡萃取模組資料(1) 裡的範例搭配使用就可以簡單地取得。要注意的是取得的資料"material.index",這是一個整數型態,而非名稱,所以是有順序性的,這個索引對應的陣列,這個對應的陣列是"mesh.materials",也就是範例的"matList",透過這個陣列加上"face.material_index"就可以完整萃取到對應資訊。


參考資料

[ docs.blender.org ] Blender 2.92.0 Python API Documentation


相關文章

從Blender裡萃取模組資料(9)

2021年3月17日 星期三

在 Blender 裡使用高面數模型製作 Normal map

 在 Blender 裡使用高面數模型製作 Normal map

前言

  在 Blender 2.7 時就支援用高面數模型製作 Normal map ,但不知是太久沒操作,在 2.9 時操作遽然失敗了!所以最近從新複習了一下這個操作,並在此做個紀錄。


內容

  在開始前先準備要使用的低面數模型,這裡使用的就是單純的 cube ,如下圖

低面數模型與 Shader 的設定狀況

圖中左側為低面數模型,其實是 Blender 預設的 cube 模型,右側為 Shader 的狀況,要新增一個 Texture node ,這個就是計算完的 Normal map ,在圖的"1"處,在圖中的"2"處,新增 Normap Map node ,圖中圈起來的地方要設定要使用哪一組 UV ,這裡選擇了預設的那一組。接著來看高面數的模型,如下圖 

高面數模型與 Shader 的設定狀況

高面數模型的上方有突起,前方有凹陷, Shader 使用的是預設的 Material ,這裡要注意高面數模型與低面數模型不要共用同一個 Material


  準備模型後就可以開始 Bake ,操作如下

設定 Render properties

選取高面數與低面數模型,選取前注意 3D View 不要處在 Edit Mode ,要在 Object Mode ,選取要注意有"順序性",先選高面數模型再選低面數模型,接著設定 Render properties ,點選圖中"2"處後,將 Render Engine 改成"cycles",接著往下找到 Bake 項目如下圖設定

設定 Bake 選項

選取 Bake Type 為 Normal ,也就是圖中的"1"處,接著選取 Selected to  Active ,並將 Extrusion 設為"0.5",最後按下"Bake"(圖中的"4"處)後就可以產生 Normal map 。範例產生的結果如下圖
範例產生的 Normal map