顯示具有 python 標籤的文章。 顯示所有文章
顯示具有 python 標籤的文章。 顯示所有文章

2022年3月14日 星期一

考慮使用 Decimal 來解決精度的問題

 考慮使用 Decimal 來解決精度的問題

前言

  最近在 Python 發生 round() 無法正常四捨五入的問題,在網上查詢後發現 Python 的 float 會有精度的問題,在此把學習的過程做個紀錄。


內容

  範例如下

from decimal import Decimal,ROUND_HALF_UP
print(round(1.115, 2) )#1.11
print(Decimal('1.115').quantize(Decimal('.00'), ROUND_HALF_UP) )#1.12


1.115 在作 round() 會期望得到 1.12 ,但事實上卻是 1.11 ,要解決這個問題要透過 decimal 模組提供的 Decimal 來處理精度的問題,要四捨五入時透過 quantize()  來完成,這樣就可以得到期望的 1.12 。


參考資料

[ kirin.idv.tw ] Python – decimal 模組教學 – 四捨五入的正確作法

2022年2月28日 星期一

在 Python 比對中文字串

 在 Python 比對字串

前言

  以前一直有不要在 Python 處理中文的印象,但最近發現 Python3 以後對中文字串已改善,並順便做個學習,在此做個紀錄。


內容

  最近都在用正則表達式,發現就算用中文有沒太大的問題,但那是 JavaScript ,在 Python 也是如此嗎?就結果來說是的,由於我入門 Python 時是在  2 的時期,那時的中文處理很麻煩加上當時我用不到,所以留下了盡量不要用 Python 來處理中文字串的印象,但最近一查發現 Python3 以後對於中文字串處理就變得相當友善,實際範例如下

str = '一'
if str == '一':
    print('equal')
if str in '一二':
    print('contained')
print(str.find('一二')!=-1)#False


要比對字串是否相等時使用"=="就可以,第二個應用是使用"in"來確定字串是否在某個字串裡,這個用法非常直覺,第三個應用是使用"find()",但要注意"find()"的結果是回傳字串的索引值,如果找不到匯回傳"-1"。


參考資料

[ blog.csdn.net ] python 中文字符串比较

2022年2月7日 星期一

用 Python 的正規表達式檢查IP

 用 Python 的正規表達式檢查IP

前言

  在先前的 用正規表達式檢查IP 裡使用正規表達式來檢查 IP ,這次用 Python 來完成,在此做個紀錄。


內容

  範例如下

import re
str = '128.0.0.1'
regEx = re.compile( '^((\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.){3}(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))$' )
#check is match?
res = regEx.search( str )
if res is not None:
    print('Match')
else:
    print('Not match')


這次的正規表達式可以參照 用正規表達式檢查IP 裡的作法,但這次字串的字尾不須加".",所以正規表達式看起來相當冗長,單純的將上一次的正規表達式的檢查次數由 4 次改成三次,並在"."後新增檢查 255 的正規表達式即可,這裡用起來跟 JavaScript 的感覺是一樣的。


相關文章與資料

用正規表達式檢查IP


2022年1月10日 星期一

在 Python 初探正規表達式

 在 Python 初探正規表達式

前言

  在先前的 在 JavaScript 初探正規表達式 裡使用正規表達式,想了一下 Python 也是預設就支援正規表達式,所以就順便做個學習,在此做個紀錄。


內容

  範例如下

import re
str = 'xxxabbc,xxxabc'
failedStr = 'failed string'
regEx = re.compile( 'ab*c' )
print( regEx.search( str ).span() )#(3, 7)
#check is match?
res = regEx.search( failedStr )
if res is not None:
    print('Match')
else:
    print('Not match')
#
print( regEx.findall( str ) )#['abbc', 'abc']


要在 Python 使用正規表達式需要透過 re 模組,透過 re.compile() 可以做出像 JavaScript 的 RegExp 一樣的類別,但這次不需要在字尾加'g',透過類別的 search() 可以取得比對的結果,但要注意回傳的是一個類別,利用回傳的類別的 span() 可以得到各個結果的起始位址,這裡有個不太直覺的地方, search() 在當沒有任何結果符合狀況時會回傳 None ,所以直接 search() 完喚起 span() 其實是有危險的!最後可以透過 findall() 來取得所有符合的結果,這裡回傳的就是陣列,就算沒有符合的結果也會回傳空陣列,可以安心使用。


參考資料

[ www.runoob.com ] Python 正则表达式


相關文章與資料

在 JavaScript 初探正規表達式

2021年10月25日 星期一

關於 Python 的繼承問題

 關於 Python 的繼承問題

前言

  在之前的 Javascript 的 class 繼承的注意事項 實驗了 JavaScript 的 super 的用法,這次把相同的做法放在 Python 會有一樣的結果嗎?在此把學習的過程做個紀錄。


內容

  範例如下

class BaseClass:
    def __init__(self):
        pass
    def call(self):
        print('I am BaseClass.')
#
class ExClass(BaseClass):
    def __init__(self):
        pass
    def call(self):
        print('I am ExClass.')
#
class ExClass1(ExClass):
    def __init__(self):
        pass
    def call(self):
        #Follow  code will failed!
        #super().super().call()
        super().call()
        print('I am ExClass1.')
#
a = ExClass1()
a.call()


實驗的結果和 JavaScript 一樣, super 只能單次使用,不能連著用,看來這個特性和 JavaScript 是一致的!


相關文章與資料

Javascript 的 class 繼承的注意事項

2021年10月11日 星期一

關於 Python 的 if in 語法

 關於 Python 的 if in 語法

前言

  以前我一直認為 for 、 if  與 in 是各自獨立的語法,所以不會在 if 的時候使用 in ,但最近發現這個觀念是錯的,應該是 for in 與 if in 這樣分才對,所以就做個學習,在此做個紀錄。

 

內容

  範例如下

str = 'Hello world'
if 'world' in str:
    print( 'True' )
else:
    print( 'False' )
#
res = 'True' if 'world' in str else 'False'
print( res )


範例的開頭使用 if in 來檢查 str 裡是否有'world',如果有就顯示'True',相反則是'False',接著是個人覺得很不直覺的語法,把 if else 一行解決的語法與 if in 搭配使用的範例,不直覺地點就是成立的結果要打在前方,語法就是覺得讀起來有些不舒服,不過卻看到語法教學網站推薦使用就是了。


參考資料

[ jarvus.dragonbeef.net ] Python技巧:新手提升效率的必學語法

2021年6月16日 星期三

Python 的鏈式比較

 Python 的鏈式比較

前言

  最近在 [ www.itread01.com ] 【python】簡化鏈式比較 發現 Python 的比較語法有個特別的寫法叫"鏈式比較",在此把學習的過程做個紀錄。


內容

  先來看看語法範例,如下

value = 60
#Old style
if 20 < value and value <100:
    print('True')
else:
    print('False')
#New style
if 20 < value < 100 :
    print('True')
else:
    print('False')


在一般的程式語言會需要做兩次比較的描述,但 Python 提供可以一次就比較且語法直觀,不過我進一步實驗以下

value = 60
if 20 < value != 100:
    print('True')


結果是會列印"True",也就是語法不限於小於或大於的比較運算,只要這個語法想成會拆開個別的比較運算後再用'and'來得到結果就不覺得這語法神奇了。


參考資料

[ www.itread01.com ] 【python】簡化鏈式比較

 


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)

2020年10月20日 星期二

在 Python 中使用 Qt

在 Python 中使用 Qt

前言

  Qt 不只提供 C++ 使用,也提供 Python 來使用,不過使用需要一些前置作業,在此把學習的過程做個紀錄。


內容

  在 Python 裡使用 Qt 必須先安裝 Qt 相關套件,這些套件會安裝在 Python 的套件管理,透過以下命令來安裝

pip3 install PyQt5
pip3 install pyqt5-tools


pip3 是 Python 自帶的套件管理,在 Windows 時並不能直接執行,這個命令會在"(安裝目錄)/Scripts",安裝完後到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑

(HelloQt' directory)/PyQt/Basic ,這次的專案檔的附檔名為".pyproject",和 C++ 的".pro"不一樣。
如果要自己創建一個專案的話請依下圖操作
創建專案的操作


接著為專案命名,如下圖
專案的命名


這裡取好專案名後就下一步到下一步驟
設定預設專案的類別

這裡需要設定設定預設專案的名稱,這個名稱會關係到專案檔的名稱,這個預設的類別會用不到,所以不用在乎類別的名稱,要在乎只有專案名稱,接著到下一步驟
完成創建專案精靈

在完成專案後需要在創建一個名為"MainWindow"的類別,創建的步驟如下
創建類別精靈


依上圖操作後到下一步
設定類別名稱與相關設定

步驟2選擇"PyQt5",繼承類別選擇"QMainWindow",接著到下一步驟
完成創建類別精靈

在創建完類別後還要在創建一個".ui"的設計檔,有這個設計檔可以在 Qt Creator 裡直接編輯 UI,就像 C++ 使用 Qt 一樣,操作如下
創建設計檔的精靈

操作完到下一步
選擇設計檔的樣本

選擇"MainWindow"後就到下一步
設定設計檔的名稱

命好設計檔檔名後到下一步
完成新增設計檔精靈


完成後在專案的狀況如下
完成上述的新增後專案的狀況

 
雙擊畫面的"mainwindow.ui"可以直接到設計介面修改設計,範例的設計如下
範例的設計

範例只新增一個 PushButton ,接著看到"mainwindow.py",程式碼如下
# This Python file uses the following encoding: utf-8
from PyQt5 import QtCore , QtWidgets , uic
from PyQt5.QtCore import pyqtSlot , qDebug

class MainWindow( QtWidgets.QMainWindow ):
    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        super().__init__()
        uic.loadUi("mainwindow.ui", self)
        #BindEvent
        self.pushButton.clicked.connect( self.onPushButtonClicked )
    @pyqtSlot()
    def onPushButtonClicked(self):
        qDebug( 'Hello' )

在建構式裡會透過"uic.load()"來讀取設計檔,接著透過"connect()"來綁定事件,在要綁定的 Function 前要加"@pyqtSlot()",這個地方跟 C++ 的格式很像,事件的內容就單純列應偵錯訊息,接著看到"pyqt_basic.py",程式碼如下
# This Python file uses the following encoding: utf-8
import sys
from PyQt5 import QtWidgets
from MainWindow import MainWindow
#Follow code add by wizard,but it will not used
#class PyQt_Basic:
#    def __init__(self):
#        pass  # call __init__(self) of the custom base class here

if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    #Follow code add by wizard,but it will not used
    # window = PyQt_Basic()
    ## window.show()
    #
    mainWin = MainWindow()
    mainWin.show()
    sys.exit(app.exec_())

開頭有個類別"PyQt_Basic",這是由精靈產生的類別,但由於用不到救註解掉。在主程式的開頭有精靈產生程式碼,由於用不到一樣註解掉,接著將"MainWindow"建出來,並喚起"show()"來顯示即可。

2019年6月24日 星期一

在Blender裡製作可多選的按鍵UI

在Blender裡製作可多選的按鍵UI

前言

  最近想再匯出模組的工具裡新增可以過濾物件的選項,它跟一般的UI不太一樣,這個UI必須可以"多選"而非"單選",就像Blender自帶的FBX匯出一樣,如下圖
Blender的可多選的按鍵UI

在實作後卡了一陣子,在此把學習的過程做個紀錄。

內容

  在Export時新增UI,可以參照在Blender的script裡使用FileBrowser選取檔案,這裡就說明關鍵的部分,如下
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',
)

如果照著範例打會呈現以下
只能單選的按鍵UI
這樣的UI會變成只能"單選",本以為Blender會另外建立一種Property來解決,但google後找不到相關的文章(可能我的關鍵字沒下好)。在想了一陣子後想到Blender的FBX匯出就有一個這樣的UI,就去翻了一下該外掛的原始碼,就知道是如何做的,範例如下
type = EnumProperty(
  name="Example Enum",
  description="Choose between two items",
  options={'ENUM_FLAG'},
  items=(
    ('OPT_A', "First Option", "Description one"),
    ('OPT_B', "Second Option", "Description two")
  ),
  default={'OPT_A','OPT_B'},
)

可以看到要對"options"設定參數,並且要注意預設值的部分不再是string,而是一個dictionary,改完後就可以多選了。

參考資料

Blender Documentation Contents

相關文章

在Blender的script裡使用FileBrowser選取檔案

2019年6月10日 星期一

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

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

前言

  最近匯出模組時發現沒 Texture 很不方便,所以就來研究如何取得 Texture 的資料,在五做個紀錄。

內容

  萃取Texture的資料意外的簡單,看以下範例
import bpy
tagObj = bpy.data.objects['Cube']
if tagObj.active_material != None:
    for curTexSlot in tagObj.active_material.texture_slots:
        if curTexSlot == None or curTexSlot.use!=True:
            continue
        #
        if curTexSlot.texture.type != 'IMAGE':
            continue
        #
        print('Slot name:',curTexSlot.name) 
        print('Image name:',curTexSlot.texture.image.name) 
        print('Image width:',curTexSlot.texture.image.size[0])
        print('Image height:',curTexSlot.texture.image.size[1])
        print('Image format:',curTexSlot.texture.image.file_format)
        print('Image path:',curTexSlot.texture.image.filepath_from_user() )
        #Pixel data
        print('Image pixel data amount:',len(curTexSlot.texture.image.pixels) )
        
        

範例先檢查是否有 Material 的資料,接著會取 texture_slots,預設會有18個,要注意 slot 可能事空的,範例有過濾該 Texture 是否是使用中,這點可依據需要來取捨。接下來是檢查 Texture 的 type ,範例指讀取 Image 的型態,這個檢查是"必要"的,當 type 是 Image 時, texture.image 才會有資料。資料對應編輯器的顯示可以參考下圖
資料對應編輯器的顯示

width 與 height 很直覺的就在 image 的 size, format 會顯示 Blender 支援的圖片格式,範例的話這個數值會是 PNG ,在官網 Blender Documentation Contents 的解說如下

file_format¶
Format used for re-saving this file

BMP BMP, Output image in bitmap format.
IRIS Iris, Output image in (old!) SGI IRIS format.
PNG PNG, Output image in PNG format.
JPEG JPEG, Output image in JPEG format.
JPEG2000 JPEG 2000, Output image in JPEG 2000 format.
TARGA Targa, Output image in Targa format.
TARGA_RAW Targa Raw, Output image in uncompressed Targa format.
CINEON Cineon, Output image in Cineon format.
DPX DPX, Output image in DPX format.
OPEN_EXR_MULTILAYER OpenEXR MultiLayer, Output image in multilayer OpenEXR format.
OPEN_EXR OpenEXR, Output image in OpenEXR format.
HDR Radiance HDR, Output image in Radiance HDR format.
TIFF TIFF, Output image in TIFF format.
AVI_JPEG AVI JPEG, Output video in AVI JPEG format.
AVI_RAW AVI Raw, Output video in AVI Raw format.
FRAMESERVER Frame Server, Output image to a frameserver.
H264 H.264, Output video in H.264 format.
FFMPEG MPEG, Output video in MPEG format.
THEORA Ogg Theora, Output video in Ogg format.
XVID Xvid, Output video in Xvid format.

路徑的部分透過 filepath_from_user() 來取得,取得後會是"絕對路徑",如果透過 filepath 或 filepath_raw 這兩個 Property取得會得到 Blender定義的路徑(範例的數值是 //color.png ),就我個人的需求來這個是數值目前用不到,所以範例只展示了使用"絕對路徑",有了"絕對路徑"就可以讀檔將檔案的資料直接讀出,這也是匯出會把整張圖匯到檔案理會需要的。最後會看到 pixels 這個 Property ,這個 Property 可以直接取得 Pixel 的資料,資料的型態是 List ,每個資料都是 float ,資料排列的方式是從"左下"座標為基準排列 Piexl 的資料,如果習慣用"左上"排列,記得做上下要顛倒的處理,每個 Pixel 的組成是4個float,依據 RGBA 的順序來排列。 pixels 這個 Property 我最後並沒有用到,就像之前說的,有絕對路徑就符合需求了,單純只是研究用。

2021/03/29 修正:
  由於 Blender 2.8 以後的 Material 有更動,上述方法不再適用,可以參考 從Blender裡萃取模組資料(11)

參考資料

Blender Documentation Contents

相關文章

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

2018年11月19日 星期一

ndk-build的使用心得

ndk-build的使用心得

前言

  最近將繪圖引擎跨到Android,在繪圖API的部分會面臨到選擇GLES的版本的問題,還有像是GLES3只有64位元支援,如果只靠修改Android.mk與Application.mk的話會沒辦法做出像是組態(Configuration)的效果,所以對ndk-build做了研究,在此做個紀錄。

內容

  ndk-build本身不提供組態(Configuration)的功能,但可以透過參數指定Android.mk與Application.mk,這做法可以做出組態(Configuration)的效果,但只要想想組態越多,Android.mk與Application.mk的數量也會隨著組態而增多,GLES的版本目前有GLES2、GLES3、GLES3.1與GLES3.2,如果用這個方法就會有8個檔案要維護,加上共用的部分還要隔開的成本,做了一下我就放棄這個做法,因為成本有點高。

  在google一陣子後發現ndk-build可以直接從命令列新增或修改在Android.mk與Application.mk的參數,比方說"APP_ABI"這個參數,在入門教學中會把該參數寫在Application.mk,用來決定要建置的ABI,但事實上是可以用ndk-build的命令列來修改,命令如下
ndk-build APP_ABI=arm64-v8a

當用這個命令後,請移除Application.mk裡的"APP_ABI"的設定,不然ndk-build社的數值會被覆蓋,如果要設定多個參數就用空白隔開即可。只靠這個方法來實現組態會不太夠,必須再搭配makefile的if語法,語法的範例如下
ifeq ($(GLES_VERSION),GLES2)
GLES_LIBS := -lGLESv2
endif
ifeq ($(GLES_VERSION),GLES3)
GLES_LIBS := -lGLESv3
endif
ifeq ($(GLES_VERSION),GLES3_1)
GLES_LIBS := -lGLESv3
endif
ifeq ($(GLES_VERSION),GLES3_2)
GLES_LIBS := -lGLESv3
endif

範例中使用"GLES_VERSION"這個變數來決定最後的要載入的LIB,"GLES_VERSION"是自訂的,搭配之前的方法可以在ndk-build的命令列隨喜好來修改,這樣不論是選GLES版本或者是只能建64位元版本都可以解決了!

  ndk-build在指定ABI的版本來建置後,會把非建置版本的ABI移除,例如APP_ABI=arm64-v8a時,x86_64、armeabi-v7a與x86都會被移除,這個問題目前解決的方案不是很好,就是每個ABI都分不同資料夾來建置,可以在ndk-build的命令列修改"NDK_OUT"與"NDK_LIBS_OUT"來完成,這個方法可行,但就是覺得不太優雅,有機會再找找有沒有優雅的解決方案。

  ndk-build的clean有點討厭,它只為清除LIB,但不會清除OBJ,如果需要完整重建的話,就手動清空或是寫個命令來清除。

  最後來說一下命令的部分,我個人是使用Python來執行ndk-build並選擇組態(Configuration),不選擇Shell與bash只是單純地不熟,但Python雖然跨平台,但有新舊版本的問題(Python2.7與Python3),這部分的實現目前就這樣解決,如果有更好的方法以後再來說明。

參考資料

Application.mk
ndk-build

2018年10月1日 星期一

使用python來備份SVN

使用python來備份SVN

前言

  備份SVN這件事以前都不是我會負責的事,但一定有做,最近添購了一台NAS,想想是個機會,就來研究一下,在此做個紀錄。

內容

  如果需要備份SVN的話就需要"svnadmin",所以請先確認該命令是否可以使用。備份SVN的命令如下
svnadmin dump c:\svnrepo --incremental > c:\backup\xxx.dump

紅色的部分是檔案庫的位置,藍色的則是輸出的備份檔,請依據需求更改。如果要用備份檔來還原檔案庫的話命令如下
svnadmin load c:\svnrepo < c:\backup\xxx.dump

紅色的部分是還原檔案庫的位置,藍色的則是使用的備份檔。整個過程用起來相當簡單,但備份這件事通常會希望是"自動執行"的,而不是需要用"手動執行"命令才動作,所以接著來介紹如何自動執行。

  排程這個功能在Windows與Linux都有這個功能,它可以用來每隔一段時間來時執行某個命令,幸運的,買來的NAS有視窗化的操作來編輯排程,所以這部份很快就完成了,至於Windows與Linux的排程實際上怎麼使用以後有機會再研究。

  使用Python來執行備份命令,為什麼不直接用bash呢?唯一的考量就是跨平台,加上Python使用起來比較靈活,所以就決定用Python了。執行的時候通常會希望可以依據備份的時間來分資料夾,所以我實作了以下的範例碼
import os
import time
backUpRootDir = '/homes/hosee/SVNBackUp/'
backUpFileName = 'svnBackUp.dump'
#
dirName = time.strftime("%Y%m%d_%H%M%S", time.localtime() )
os.system('mkdir -p '+backUpRootDir+dirName)

os.system('svnadmin dump /homes/hosee/svn/repo --incremental > '+backUpRootDir+dirName+'/'+backUpFileName)

這個範例碼會依據備份的時間製作資料夾,並在該資料夾儲存備份檔。範例碼的路徑請依據需要修改,如"backUpRootDir"與svnadmin的檔案庫位置。最後,就是只要用排程來啟動Python的script就完成了。

參考資料

subversion的備份hotcopy以及dump的使用

2018年9月3日 星期一

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

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

前言

  續前篇從Blender裡萃取模組資料(7),這次說明萃取Skeleton的動畫,在此做個紀錄。

內容

  如果使用前篇的範例來萃取Skeleton的動畫,應該會發現竟然完全沒問題,可以正常執行,但若仔細一看"Data path"的欄位,就會發現它有何不同了,就拿"location"來說,在Skeleton的動畫可能會是"pose.bones["spine"].location",其中的"spine"為該Bone的名稱。這裡有個陷阱!如果你直覺地用字串的split()來parse這個"Data path"的欄位的話會有問題,問題會發生在Bone的名稱是可以包含".",舉例來說如果Bone的名稱是"spine.001"的話,"Data path"就會變成"pose.bones["spine.001"].location",接著split()後就變成"[pose, bones["spine, 001, location]",明顯的,Bone的名稱會被切到,還必須注意Blender的名稱可以很自由,名稱裡可以出現單引號或雙引號,所以需要特製的Parse。

  接著用前篇的範例為基礎,製作出以下範例
import bpy

def ShowActionInfo(act):
  print('animation name',act.name)
  for curve in act.fcurves:
    print('Data path:',curve.data_path,' ',end='')
    print('')
    if curve.data_path == 'location':
        print("Data from transform.location")
    elif curve.data_path == 'rotation_euler':
        print("Data from transform.rotation_euler")
    elif curve.data_path == 'rotation_euler':
        print("Data from transform.rotation_quaternion")
    elif curve.data_path == 'scale':
        print("Data from transform.scale")
    elif curve.data_path[:12] == 'pose.bones["':
        namelastIndex = curve.data_path.rfind('"')
        if namelastIndex >= 0:
            boneName = curve.data_path[12:namelastIndex]
            propertyNameIndex = curve.data_path.rfind('.')
            if propertyNameIndex >= 0:
                propertyName = curve.data_path[ (propertyNameIndex+1):]
                if propertyName == 'location':
                    print('Bone:',boneName,' Property:transform.location')
                elif propertyName == 'rotation_euler':
                    print('Bone:',boneName,' Property:transform.rotation_euler')
                elif propertyName == 'rotation_quaternion':
                    print('Bone:',boneName,' Property:transform.rotation_quaternion')
                elif propertyName == 'scale':
                    print('Bone:',boneName,' Property:transform.scale')
                else:
                    print('Unknown property:',propertyName)
            else:
                print('Parse skeleton property error!')
        else:
            print('Parse skeleton bone name error!')
    else:
        print("Unknown data path!")
    print('Array index:',curve.array_index,' ',end='')
    print('')
    for keyFrame in curve.keyframe_points:
      print('Key time:',keyFrame.co[0],' ',end='')
      print('Key value:',keyFrame.co[1],' ',end='')
      print('')
#
def ShowAnimationInfo(obj):
  if obj.animation_data == None:
    print("No animation data in object!")
    return
  #Check actived action...
  actionCon = 0
  if obj.animation_data.action != None:
    ShowActionInfo(obj.animation_data.action)
    actionCon+=1
  #Check action in NLA tracks...
  for track in obj.animation_data.nla_tracks:
    for strip in track.strips:
      ShowActionInfo(strip.action)
      actionCon +=1
  print("Animation amount:",actionCon )
#
tagObj = bpy.data.objects['metarig']
ShowAnimationInfo(tagObj )

可以看到ShowActionInfo()裡多了非常多的if判斷,目前沒找到比較簡易的寫法,目前的作法以直觀與好懂為原則, 所以程式碼相當長。Bone的名稱如之前所說由於編輯器可以相當自由命名,所以用rfind()來找,property的部分也 是使用rfind()來找也請注意。

參考資料

Blender Documentation Contents

相關文章

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

2018年8月27日 星期一

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

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

前言

  續前篇從Blender裡萃取模組資料(6),這次說明動畫的資料如何萃取,在此做個紀錄。

內容

  在開始前,先看看下圖
Blender的動畫資料
在圖中,我將動畫的狀況分為3種,沒有動畫、只有一個動畫與多個動畫這3種,在"沒有動畫"時,物件底下不會有Animation的node,在"只有一個動畫"時,Animation的node底下會有一個"CubeAction"的node,這個node就是啟用中(Actived)的動畫的資料,再來就是"多個動畫"時會發現底下多了個"NLA Tracks"的node,把整個tree展開,會發現底下會多一個"MultiAnimationCubeAction",這個就是非啟用中的動畫資料,如果有多筆非啟用中的動畫的話,"NLA Tracks"的node下就多有多筆"Action Stash"。接著,來看看動畫的內容是如何存放的,如下圖
Blender的動畫內容資料
可以看到很多個Curve,可以想像每個Curve其實就是一個"float"的資料,所以"Location"的動畫被切成3個Curve,再來會看到右側有一些"菱形",而每個"菱形"就代表一個Keyframe,裡面紀錄的是時間與數值。

  接著就來萃取動畫的資料,範例如下
import bpy

def ShowActionInfo(act):
  print('animation name',act.name)
  for curve in act.fcurves:
    print('Data path:',curve.data_path,' ',end='')
    print('Array index:',curve.array_index,' ',end='')
    print('')
    for keyFrame in curve.keyframe_points:
      print('Key time:',keyFrame.co[0],' ',end='')
      print('Key value:',keyFrame.co[1],' ',end='')
      print('')
#
def ShowAnimationInfo(obj):
  if obj.animation_data == None:
    print("No animation data in object!")
    return
  #Check actived action...
  actionCon = 0
  if obj.animation_data.action != None:
    ShowActionInfo(obj.animation_data.action)
    actionCon+=1
  #Check action in NLA tracks...
  for track in obj.animation_data.nla_tracks:
    for strip in track.strips:
      ShowActionInfo(strip.action)
      actionCon +=1
  print("Animation amount:",actionCon )
#
tagObj = bpy.data.objects['Cube']
ShowAnimationInfo(tagObj )

在ShowAnimationInfo()的部分,只要是在辨識動畫的數量,要注意一點的是啟用中的動畫可能會是None,在編輯器編輯時是可以將它調成空的,ShowActionInfo()的部分,有"Data path"與"Array index"兩個資料,"Data path"可能會是"location"、"rotation_euler"或"scale"...等,"Array index"則是個數值,假設得到的結果為"location"與"1",也就是location[1],進一步說就是location.y,請依此類推,"Key time"舊式下方時間條的數值,"Key value"就是數值。

參考資料

Blender Documentation Contents

相關文章

從Blender裡萃取模組資料(6)
從Blender裡萃取模組資料(8)

2018年8月20日 星期一

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

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

前言

  續前篇從Blender裡萃取模組資料(5),這次來說明SkinedMesh的權重資料如何取得,在此做個紀錄。

內容

  SkinedMesh的權重資料指的是什麼呢?可以參考下圖
SkinedMesh的權重資料
在SkinedMesh裡的每個Vertex都會有個別的權重資料,裡面會記錄那些Bone對該Vertex的引響力,通常小於1並且不會是負數,範例圖可以看到該Vertex有兩個Bone對它有引響力,分別是"Bone"與"Bone.002",而數值分別為"0.906"與"0.235"。

  接著,就來看如何取得權重資料,範例程式如下
import bpy

tagMesh = bpy.data.meshes["Cube"]
#Create bmesh from mesh
tagBMesh = bmesh.new()
tagBMesh.from_mesh(tagMesh)
#
tagBMesh.verts.ensure_lookup_table()
layer_deform = tagBMesh.verts.layers.deform.active

#Start extract
baseIndex = 0
for face in tagBMesh.faces:
  for i in range(len(face.loops) ):
    print("Position:",face.loops[i].vert.co," ",end="")
    print("Normal:",face.normal," ",end="")
    if layer_deform!=None:
      vertData = tagBMesh.verts[face.loops[i].vert.index]
      weightKeyList = vertData[layer_deform].keys()
      for weightKey in weightKeyList:
        print("WeightIndex:",weightKey," WeightValue:",vertData[layer_deform][weightKey]," ",end="")
    
  print("Loop vertex end")
  loopLen = len(face.loops)
  for i in range( len(face.loops) - 2):
    print("index0:",baseIndex," ",end="")
    print("index1:",baseIndex + i + 1," ",end="")
    print("index2:",baseIndex + i + 2," ",end="")
  print("Loop index end")
  
#Free bmesh...
tagBMesh.free()

這個範例相似於從Blender裡萃取模組資料(1)的範例,但這次會去拿權重的資料。在開頭新增了取的"layer_deform",如果這個值是None表示這個Mesh沒有權重資料,取得權重的部分可以看到事先取得Vertex的資料後再取得權重資料,跟之前的"位置"與"Normal"並不一樣,這一點要注意,再來是取得"weightKeyList",在於List的Key來取得權重數值,這個部分有點特別,"weightKey"並不是Bone的名稱,而只是一個數字!這個數字其實是"Armature.bones"的陣列索引 ,如果忘記"Armature.bones"可以參考從Blender裡萃取模組資料(4)。所以如果要得到名稱的話還需要搭配Armature的資料才可以取得。權重數值的部分取出來就會跟編輯器看到的是一樣的不需要再處理。

參考內容

Blender Documentation Contents

相關文章

從Blender裡萃取模組資料(5)
從Blender裡萃取模組資料(7)

2018年8月13日 星期一

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

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

前言

  續前篇從Blender裡萃取模組資料(4),這次會說明Skeleton的transform如何取得,在此做個紀錄。

內容

  在上一篇從Blender裡萃取模組資料(4)中提到了Hierarchy的取得,但Skeleton的Bone還必須包含"Position"與"Rotation"的資訊,這次就坐明這兩種資訊如何取得。
  先來看看只考慮"Position"的部分如何取得,範例如下
import bpy

def ShowBonePosition(bone):
  if bone.parent != None:
    boneWorldPos = bone.head_local
    parentBoneWorldPos = bone.parent.head_local
    print(' Bone pos:',boneWorldPos - parentBoneWorldPos,end='')
  else:
    print(' Bone pos:',bone.head_local,end='')

def ShowBoneInfo(bone,levelCon=0):
  for i in range(levelCon):
    print("  ")
  #
  print('Bone name:',bone.name,end='')
  ShowBonePosition(bone)
  print('')
  #
  for childBone in bone.children:
    ShowBoneInfo(childBone ,levelCon+1)

tagObj = bpy.data.objects["metarig"]
if type(tagObj.data) == bpy.types.Armature:
  armatureData = tagObj.data
  rootBoneList = []
  for bone in armatureData.bones:
    if bone.parent == None:
      rootBoneList.append(bone)
  #
  for rootBone in rootBoneList:
    ShowBoneInfo(rootBone)

搭配前一篇的範例,這次新增"ShowBonePosition()",取得的方法並不困難,Bone的"head_local"代表的是世界座標,所以在有Parent的狀況時,要將它轉成本地座標。

  接著來看看"Position"與"Rotation"如何取得,範例如下
import bpy

def ShowBonePostionAndRotation(bone):
  quat = bone.matrix.to_quaternion()
  if bone.parent != None:
    boneWorldPos = bone.head_local
    parentBoneWorldPos = bone.parent.head_local
    localPos=boneWorldPos - parentBoneWorldPos 
    parentRot = bone.parent.matrix_local.to_3x3().to_quaternion()
    invParentRot = parentRot.inverted()
    print('Bone pos:',invParentQuat.to_matrix() * localPos,end='')
  else:
    print(' Bone pos:',bone.head_local,end='')

  print(' Bone rot:',quat,end='')

def ShowBoneInfo(bone,levelCon=0):
  for i in range(levelCon):
    print("  ")
  #
  print('Bone name:',bone.name,end='')
  ShowBonePostionAndRotation(bone)
  print('')
  #
  for childBone in bone.children:
    ShowBoneInfo(childBone ,levelCon+1)

tagObj = bpy.data.objects["metarig"]
if type(tagObj.data) == bpy.types.Armature:
  armatureData = tagObj.data
  rootBoneList = []
  for bone in armatureData.bones:
    if bone.parent == None:
      rootBoneList.append(bone)
  #
  for rootBone in rootBoneList:
    ShowBoneInfo(rootBone)

這次的做法很不一樣,理由很簡單,如果只是單單需要知道Bone的旋轉,利用Bone的matrix直接取得就是Local的旋轉,不需要再轉換,但"Position"是會被Parent的旋轉引響的,所以要透過Bone的matrix_local(這個是世界旋轉)的"逆旋轉"來反算"Position"。

參考資料

Blender Documentation Contents

相關文章

從Blender裡萃取模組資料(4)
從Blender裡萃取模組資料(6)