Armory3D: RenderTarget & Canvas UI

## ?? RenderTarget & Canvas UI
Armory Engine 邏輯節(jié)點編程中 Graphics 處理的節(jié)點有四類:
- [Draw] 對畫布繪制簡易圖形、圖像紋理、曲線等等,配合 `On Render2D`、Canvas UI Trait 使用;
- [Canvas] 節(jié)點分類下用于 Armory2D 設(shè)計好的 Canvas UI 元素的交互,比如設(shè)置或獲取文本;
- [Post-Process] 后期處理添加圖像處理效果,比如 LUT 紋理,一方面提升畫面質(zhì)量,同時又減少渲染運算;
- [Render Path] 渲染路徑,對游戲渲染引擎進行編程,定制渲染過程。
文檔來源??https://github.com/Jeangowhy/opendocs/blob/main/Haxe.md
[LUT Textures] 是一種基于查表算法的圖像后期處理技術(shù),LUT 紋理圖像就是一張顏色對照表,LUT 意思是對紋理圖像進行查表,Lookup Table Texture,找到指定的色彩替換到原圖像上形成新的圖像輸出。查表是一種映射算法,比起引擎的渲染,效率極高。同時預(yù)配置不同的色彩卡可以得到不同的圖像效果輸出,操作流程可標準化,使用十分方便。參考官方的示例 [render_colorgrading]。
配色圖片的本質(zhì)就是將顏色方塊進行二維化處理。假設(shè) LUT 紋理圖片分辨率為 512 * 512,劃分為 8 * 8 的大格子,每個大格子中存有 64 * 64 個小格子,即用來存儲色彩像素點。每個小格子 X 軸表示 R 通道 Y 軸表示 G 通道,取值范圍 [0, 255]。剩下藍色分量的 B 通道放在了大格子中,從左到右,從上到下,最后將 RGB 三個分量疊加。一張顏色圖片一功能儲存 64 * 64 * 64 = 512 * 512 = 262144 種色彩。例如,一張灰度配色圖:

以下示范使用邏輯節(jié)點創(chuàng)建進度條效果,添加一個 Canvas UI,只需要一個 CProgress_bar 進度條控件。然后在 Armory Scene Traits 添加一個邏輯節(jié)點樹,節(jié)點連接如下:
1. Event - `On Update` 事件流向 `Set Variable`,再流向 `Set Canvas Progress Bar`;
2. Variable - `Integer` 使用整數(shù)變量記錄進度值;
3. 將整型變量節(jié)點連接到賦值節(jié)點的 Variable 端口,將其值經(jīng)過 `Math` 加法運算后連接到賦值節(jié)點;
4. 將整型變量連接到 `Set Canvas Progress Bar` 控制進度條的顯示;
進度條設(shè)置的 At 值如果超出 Max 范圍,則會折回再開始繪制進度,相當于從零開始。

使用 Draw 分組下的邏輯節(jié)點,使用的是 Kha Graphics2 API,主要是 `GraphicsExtension` 和 `Graphics`。需要在 `On Render2D` 回調(diào)中使用,以確保它在畫 Canvas 上下文,也就是需要在Armory Traits 列表中添加 Canvas Trait 擴展使用,否則不會觸發(fā) `On Render2D` 事件:
? ? ?DrawRectNode must be executed inside of a render2D callback.
? ? ?If used in logic node, please consult its documentation.
另外,還要將 Canvas UI 擴展掛載到場景,或者活動相機上,當然掛載到對象 Armory Trait 擴展列表也可以顯示 UI 界面元素,但是邏輯節(jié)點的 UI 繪畫相關(guān)操作不能使用它,并且引發(fā)異常。任何不配對的畫布上下文 `begin()` 和 `end()` 方法將導(dǎo) kha.graphics4.Graphics2 觸發(fā)致異常:
Draw Image 可以將圖像紋理繪制到 Canvas 2D 畫布上,注意設(shè)置圖像路徑,默認使用 Bundled 目錄存放資源文件,因為打包會碾平目錄結(jié)構(gòu),所以不要使用子目錄。Render - Armory Project 屬性面板設(shè)置了一個 Copy to Bundled 按鈕,它可以將所有外部或其它目錄中的資源復(fù)制到 Bundled 目錄。路徑指定時就填入文件名,或者 //filename.jpg 這樣的路徑。
另一種方法是直接向 khafile.js 添加腳本配置,Armory Project - Modules - Append Khafile?指定一個腳本文件,Blender 文本編輯器可以創(chuàng)建腳本文件:
? ? project.addAssets("textures/**", { notinlist: true });
由于 Zui 沒有布局容器概念,所有 Canvas UI 元素使用 XY 坐標加 Anchor 來確定如何放置。錨點屬性有兩個維度,水平和豎直方向。對應(yīng) anchorH 和 anchorV 屬性。所以注意設(shè)置好大小尺寸,配置錨點定位。注意,旋轉(zhuǎn)角度 Angle 使用的是弧度,不是 Degree。
? ? @input Left/Center/Right: Horizontal anchor point
? ? ? ? 0 = Left, 1 = Center, 2 = Right
? ? @input Top/Middle/Bottom: Vertical anchor point
? ? ? ? 0 = Top, 1 = Middle, 2 = Bottom
渲染目標 **Render Target** 是一種可以在運行時寫入數(shù)據(jù)的紋理。從引擎的角度講,渲染目標存儲顏色、法線以及 AO 等信息。從用戶的角度講,渲染目標可以視為第二個攝像機,用于捕捉圖像并保存在內(nèi)存中,并將其設(shè)置為指定對象材質(zhì)的參數(shù)。從文件存儲關(guān)系上講,渲染目標是內(nèi)存中的圖像,紋理文件是硬盤中的圖像。在不同平臺下,`RenderTarget` 接口會使用不同的對象類型實現(xiàn),比如 HTML5 對應(yīng):
? ? armsdk\Kha\Backends\HTML5-Worker\kha\Image.hx
以下是與渲染目標相關(guān)的邏輯節(jié)點:
1. `Draw Camera` 從指定攝影機的視圖中渲染場景,并將渲染目標紋理按指定大小繪制到屏幕指定坐標上。
2. `Draw Camera to Texture` 將指定相機視圖的紋理圖像繪制到指定對象的指定材質(zhì)的紋理上。
3. `Draw to Material Image` 用來繪畫 `Create Render Target Node` 創(chuàng)建的 render target;
`Draw Camera to Texture` 節(jié)點代碼中包含了自動創(chuàng)建渲染目標的功能,只需要為節(jié)點指定相機對象,以及待繪制的目標對象,Object 屬性可以留空,表示使用 owner 對象。節(jié)點一旦運行后,就會注冊一個渲染回調(diào)函數(shù),所以不能持續(xù)觸發(fā) Start 控制流。渲染回調(diào)函數(shù)對 Render Target 對象進行繪畫,繪畫內(nèi)容將取代對象材質(zhì)中的第一個上下文的第一個紋理,diffuse texture 或者 base color texture。
參考官方示范 render_to_texture,其代碼和節(jié)點代碼相比,多了一個切換相機 renderTarget 的步驟。示范代碼中則是一個相機鏡頭始終與一個新創(chuàng)建的 RenderTarget 對象綁定。
如果輪番切換渲染對象,在游戲開始運行時,首次渲染回調(diào)中可能獲取到的是一個 null,也就還未為相機設(shè)置渲染目標,需要設(shè)置渲染目標后,才能調(diào)用 `renderFrame()` 方法時得到紋理圖像。如果場景中只有一個相機,那么固定渲染目標后,內(nèi)容始終不變,就像畫面卡住一樣。并且,可能由于一開始就是沒任何內(nèi)容,導(dǎo)致相機渲染的圖像始終全黑。
另外,World 材質(zhì)屬性設(shè)置會在編譯時緩沖到環(huán)境貼圖 env_World.jpg,例如使用純色作為環(huán)境背景,Background 節(jié)點的 Strength 調(diào)整對 Armory 無效,除非 `Armory Project -> Clean` 清理項目緩存后,再重新編譯。
如果沒有繪制任務(wù)內(nèi)容到 RenderTarget,那么將它賦予對象材質(zhì)后,結(jié)果就會導(dǎo)致繪畫紋理全黑。從相機視角形成的圖像,再繪制到 RenderTarget,再通過指定的材質(zhì)紋理重現(xiàn)出來,這至少涉及兩次繪圖,兩繪制圖像的尺寸比例不一致就會引起圖像的變形。另外,相機拍攝得到的是 2D 紋理,重現(xiàn)到模型上又經(jīng)過 UV Map 的映射,這又是一個圖像變換過程。并且,在持續(xù)的渲染中,相機拍攝到的新內(nèi)容不斷疊加,這個復(fù)雜的過程會形成圖像的遞歸渲染,最終效果不一定是相像中的一樣。
armory_examples-22.06\render_to_texture\Sources\arm\MyTrait.hx
Iron 框架定義的 SceneFormat 結(jié)構(gòu)中,一個材質(zhì)對象 `MaterialData` 包含多個材質(zhì)上下文對象和一個著色器數(shù)據(jù)對象,這樣的類型層次結(jié)構(gòu)設(shè)計都是為了著色器編程定制的:
1.? `MaterialContext` 材質(zhì)上下文對象包裝紋理數(shù)據(jù);
2.? `ShaderContext` 著色器上下文對象包裝著色器程序中需要引用的數(shù)據(jù),以及符號定義;
iron\Sources\iron\object\MeshObject.hx
iron\Sources\iron\data\SceneFormat.hx
iron\Sources\iron\data\MaterialData.hx
iron\Sources\iron\data\ShaderData.hx


相機到紋理的繪制使用`Draw Camera to Texture`, 不需要指定材質(zhì)。而繪制到材質(zhì)節(jié)點是另一種使用渲染目標的方式,`Draw to Material Image`? 需要更多的參數(shù),并且這個節(jié)點使用到的 API 也更復(fù)雜,涉及了 `UniformsManager` 著色器常量管理器,以及 [Links] 著色器回調(diào)函數(shù)等等概念。如果沒有一點 Iron 框架的材質(zhì)處理的基礎(chǔ),根本沒有辦法使用這個節(jié)點。
首先,`Create Render Target Node` 創(chuàng)建的 RenderTarget,并就應(yīng)該使用 On Init 這樣的只執(zhí)行一次事件去創(chuàng)建。RenderTarget 對象創(chuàng)建好并不直接返回供使用,而是將它交給著色器常量管理器 `UniformsManager` 進行統(tǒng)一管理,需要使用對應(yīng)的資源時就通過 Links 回調(diào)函數(shù)獲取,不同資源有不同的回調(diào)函數(shù),對于紋理材質(zhì)就是 `textureLink()`。所以 `Draw to Material Image` 會調(diào)用這個 API 獲取紋理用來繪畫。
其次,`Draw to Material Image` 節(jié)點是一個“開畫布上下文”節(jié)點,所謂開畫布上下文,即打開一個畫布上下文對象,就像 `On Render2D` 那樣,然后接上各種繪畫節(jié)點,對畫布進行繪畫。因為可以對畫布進行任意尺寸的繪制,所以內(nèi)容可能會鋪滿整個游戲窗口。所繪制的紋理已經(jīng)脫離具體幾何體的邊界約束,可以將圖像繪制到屏幕的任意位置。之所以還需要指定對象、材質(zhì)等參數(shù),是因為 Iron 框架中,渲染目標需要依存于它們。
在繪畫節(jié)點分組下,有一個 `DrawImageSequenceNode` 節(jié)點,它可以將序列幀紋理圖像逐幀地繪制,同樣,這種節(jié)點只需要 Start 一次觸發(fā)就會循環(huán)地工作,只需要設(shè)置好圖像名稱的規(guī)則,如代碼所示:
? ? iron.data.Data.getImage(imagePrefix + i + '.' + imageExtension, (image: Image) -> { })
文件名如果是 0001.jpg ~ 0111.jpg 這種可能就難一點,代碼顯示只能是 abc1.jpg ~ abc111.jpg 這種規(guī)則,填充 0 值這種沒有考慮。擴展名也不需要句點,還有就是指定發(fā)繪畫區(qū)域的大小和起點坐標。從代碼邏輯上看,Wait For Load 這個參數(shù)用來在完成加載后再繪制紋理圖像,代碼循環(huán)結(jié)構(gòu)使用的是 `getImage()` 異步回調(diào),循環(huán)結(jié)束后文件可能還在加載中。
`Set Material Image` 節(jié)點也是調(diào)用 `UniformsManager` 將指定的紋理圖像注冊。
使用 `UniformsManager` 和 Iron `Uniforms` 都可以注冊著色器需要鏈接的資源,不同的是,后者可以直接注冊回調(diào)函數(shù)。而常量管理器則自己注冊好了各種回調(diào)函數(shù)來處理用戶需要注冊的資源。以下擴展腳本假設(shè)了場景中有一個名為 Plane.001 的對象,材質(zhì)有 Image Texture 節(jié)點并命名為 ImageTexture。注意,如果同時存在邏輯節(jié)點與 Trait 腳本擴展,那么邏輯節(jié)點優(yōu)先于腳本,腳本相應(yīng)注冊的資源失效。如果同時,存在 `UniformsManager` 和 Iron `Uniforms` 注冊的資源,那么常量管理器的設(shè)置優(yōu)先。`Uniforms` 注冊的回調(diào)不一定有機會調(diào)用,以下代碼就是因為 `UniformsManager` 重置了原有鏈接關(guān)系。



材質(zhì)可以使用 `Material` 節(jié)點直接從現(xiàn)有材質(zhì)中指定,或者使用 `Get Object Material` 節(jié)點從對象指定 Slot 中的材質(zhì),注意這個 Slot 從 0 計數(shù)。Blender 材質(zhì)編輯器中的 Slot 從 1 開始。另外,如果材質(zhì)未曾使用過,雖然它已經(jīng)配置好了,但是要想在游戲運行正常使用,應(yīng)該啟用 **Fake User** 保護材質(zhì)數(shù)據(jù)免被優(yōu)化掉,導(dǎo)致數(shù)據(jù)讀取錯誤。
`Draw to Material Image` 和 `Set Material Image Param` 節(jié)點在功能實現(xiàn)上有互斥,因為
前者已經(jīng)包含了紋理鏈接回調(diào)函數(shù)的設(shè)置,而后者同樣也是,只不過它還需要獨占紋理的處理過程,需要在 Blender 材質(zhì)編輯器側(cè)欄面板勾選**Armory -> Armory Material Node -> Paramerter**。
這樣才能在它的回調(diào)函數(shù)中獲取到這個材質(zhì)節(jié)點的回調(diào)處理,導(dǎo)致后果就是,其它節(jié)點讀取不到這部分數(shù)據(jù)。
并且,`SetMaterialValueParamNode` 也是需要導(dǎo)出材質(zhì)參數(shù)的配合,否則,是無法設(shè)置指定對象的材質(zhì)的紋理圖像的!這一點很容易忽略,導(dǎo)致無法意識到是哪里的問題,因為根本找到問題的根源,除非閱讀源代碼。就算是官方 Wiki 頁面,也沒有提供這些設(shè)置信息。
使用這這些節(jié)點還有一個問題,因為指定對象、材質(zhì)分開指定,很容易產(chǎn)生不一致問題。假如,對象指定 Cube,但是材質(zhì)卻是來自另外一個對象,那么最終設(shè)置的紋理是影響 Cube 還另一個對象呢?
當然,設(shè)置紋理是對哪個材質(zhì),那么就應(yīng)當產(chǎn)生的影響就歸屬于誰。這種不一致的問題似乎對軟件質(zhì)量有重大的影響。邏輯節(jié)點在設(shè)計時,應(yīng)當考慮這樣的問題,至少在節(jié)點接收了 Object 參數(shù)后,應(yīng)該將材質(zhì)端口隱藏,提供一個材質(zhì)插槽選項,提供備選供用戶選擇,而不是讓用戶去挑選另外可能導(dǎo)致節(jié)點無法正常工作的輸入。還有就是易用性問題,一般用戶根本無法想象,一個材質(zhì)設(shè)置節(jié)點竟然需要和材質(zhì)編輯器一個角落中的一個選項配合使用,即使用知道,還容易忽略。
就目前而言,Armory 僅支持 3 種材質(zhì)回調(diào)處理,它們對應(yīng)了 3 個節(jié)點:
1. `SetMaterialRgbParamNode` 設(shè)置對象材質(zhì)的顏色屬性,使用數(shù)據(jù)類型 `iron.math.Vec4`;
2. `SetMaterialValueParamNode` 設(shè)置對象材質(zhì)的數(shù)值屬性,使用數(shù)據(jù)類型 `Null<kha.FastFloat>`;
3. `SetMaterialImageParamNode` 設(shè)置對象材質(zhì)的紋理屬性,使用數(shù)據(jù)類型 `kha.Image`;
C:\HaxeToolkit\armory_examples-22.06\material_params
C:\HaxeToolkit\armory_examples-22.06\material_decal_colors
C:\HaxeToolkit\armory_examples-22.06\material_shaders
Armory 邏輯節(jié)點易用性問題最嚴重的表現(xiàn)就是 `Draw To Material Image` 這個節(jié)點上。
Armory Wiki 內(nèi)容這樣描述 Draw To Material Image:
`DrawToMaterialImageNode` 這個節(jié)點本身不負責創(chuàng)建 Render Target,需要使用自行使用節(jié)點創(chuàng)建。節(jié)點邏輯設(shè)計有點復(fù)雜,它的代碼邏輯上說明它的作用是:調(diào)用 Render Path API 為繪制紋理做準備。創(chuàng)建一個畫布上下文對象并調(diào)用 `begin()` 方法打開繪圖上下文對象,后續(xù)調(diào)用邏輯節(jié)點 Draw 分類節(jié)點,對材質(zhì)的紋理圖像進行繪畫。這些繪畫的節(jié)點連接到 Out 控制流輸出端口上,表示對打開的 Canvas 畫布上下文進行繪畫,這個打開的畫布就是材質(zhì)對應(yīng)的紋理圖像。
除了 Object 只可以留空使用默認的 owner 對象,其它參數(shù)都必須指定。材質(zhì)可以從用 `MaterialNode` 或者 `GetMaterialNode` 獲取。默認的對象會由 `ObjectNode` 填充,它返回 `LogicTree` 也就是 Trait 類型的 object 屬性。**注意,如果留空,就要掛載到正確的對象 Armory Traits 列表中!** 因為掛載的位置不正確,就獲取不到正確的數(shù)據(jù),這可以導(dǎo)致邏輯節(jié)點執(zhí)行流程中斷,并且不給出提示。
在設(shè)置參數(shù),`Object` 和 `Material` 分別指要操作的對象和其材質(zhì)對應(yīng)的 Slot,但是 `Node` 這個參數(shù)就讓人難以琢磨,是什么鬼?
這個參數(shù)是一個字符串值,會經(jīng)由 `UniformsManager` API 的 link 參數(shù)傳入,用于讀寫材質(zhì)的相應(yīng)屬性數(shù)據(jù)。所以,材質(zhì)節(jié)點中的 **Node** 這個字符串參數(shù)就是 `MaterialData` 的屬性名稱。每個材質(zhì)節(jié)點的標題都會顯示節(jié)點的名稱,也可以在側(cè)欄面板中編輯和復(fù)制它。在邏輯節(jié)點編輯器中使用材質(zhì)屬性設(shè)置節(jié)點時,就可以使用這個節(jié)點名稱。在編程中,`Node` 對應(yīng)的就是 `MaterialData` 屬性名稱。如果沒有理解這層關(guān)系,那么根本無法理解材質(zhì)屬性設(shè)置節(jié)點的使用。
重點是 `UniformsManager` 這個著色器常量管理工具,GLSL 著色器中的 `uniform` 常量由其管理。Kha.Image `createRenderTarget()` 方法創(chuàng)建一個 RenderTarget 對象,就是一個紋理圖像,然后注冊到 `UniformsManager`,后續(xù)再鏈接到著色器程序進行顯示。要對新創(chuàng)建的這個紋理繪畫,就需要按`Uniforms` 類定義的接口,使用 [Links] 回調(diào)函數(shù)對其進行處理。`UniformsManager` 注冊了默認的回調(diào)函數(shù),調(diào)用它就可以獲得紋理圖像。就如 `DrawToMaterialImageNode` 節(jié)點的代碼那樣獲取紋理,并打開畫布上下文。注意 `begin()` 和 `end()` 方法之間的 `runOutput(0)`,就是它調(diào)用后續(xù)的邏輯節(jié)點,在畫布上下文打開期間繪畫:
可以在 Blender 腳本編輯器中使用 Python 代碼逐步地探索當前選中對象的各種屬性,如下:
但是,Armory 邏輯節(jié)點編輯器中的屬性是指 iron.object.Object 對象系統(tǒng)下的對象屬性。比如,使用 `GetPropertyNode` 節(jié)點獲取屬性值,就是在查詢 Iron Objects 及其子類對象的屬性集合。屬性集合 properties 中的屬性需要使用 SetPropertyNode 定義,它們和 Object 其它屬性不同。比如 Get Object UID (GetUidNode) 節(jié)點直接通過 object.uid 或以獲取對象的 UID,但是不能通過 properties 集合獲取,因為沒有定義。
`Draw Camera` 節(jié)點有兩個控制流輸入端口,Start 端口進入繪制邏輯,注冊 `render()` 方法以處理
3D 渲染事件。注冊 `render2D()` 方法以處理 2D 渲染事件。Stop 端口輸入控制流就停止繪制,解除
事件偵聽器。檢查代碼可知,Start 觸發(fā)后就會注冊相應(yīng)的事件上偵聽器,因此不能使用持續(xù)觸發(fā)的控制流,
這會讓內(nèi)存爆滿,并且重復(fù)觸發(fā)渲染目標的繪制也不是正確的使用方式,應(yīng)該使用 `On Init` 這種單次觸發(fā)
事件流,而不能使用 `On Update` 這種持續(xù)觸發(fā)的事件流。
繪制到 Render Target 對象上的圖像來源自指定的 Camera 對象,這個相機的設(shè)置決定了來源圖像的
大小比例。調(diào)用 `createRenderTarget()` 創(chuàng)建 Render Target,其大小比例由 `Draw Camera`
節(jié)點的參數(shù)指定,包括其 position 位置屬性,后續(xù)使用紋理時決定其開始繪制位置。將相機視角的圖像
繪制到 Render target 對象這個過程就有一次變換,設(shè)置不同的寬高值就可能產(chǎn)生變形。
邏輯節(jié)點代碼中的 runOutput(0) 和 runOutput(1) 就是執(zhí)行相應(yīng)的控制流輸出端口,即對應(yīng)節(jié)點的
**On Start** 和 **On Stop**,等價于觸發(fā)兩個事件:
另外 `Get Mouse Movement` 似乎還有問題,不能獲取鼠標移動的值,以及滾輪值。經(jīng)過調(diào)試發(fā)現(xiàn),節(jié)點沒有在每次鼠標數(shù)據(jù)更新時被執(zhí)行,也就是沒有獲取最新數(shù)據(jù)。這一原由來自于將這些數(shù)據(jù)連接到了一個數(shù)組節(jié)點上,`ArrayStringNode`,對的,就是因為數(shù)組節(jié)點具有單次初始化后就不再更新數(shù)據(jù)的這個邏輯。為了將數(shù)據(jù)集中處理,還可以使用 `ConcatenateStringNode` 這樣的節(jié)點以連接多個變量構(gòu)成字符串。還需要注意的是 x y 輸出端口其實是 movementX movementY,這種端口名稱令人多有誤解。
Kha Graphics APIs 分成多種類型集合,其中 G3 缺失,只是占位而已:
1. G1 - `kha.graphics1` Just provides a framebuffer you can write into
2. G2 - `kha.graphics2` Provides a basic and very portable 2D graphics-API
3. G3 - `kha.graphics3` Old-school 3D graphics API similar to early OpenGL.
4. G4 - `kha.graphics4` 3D graphics API similar to Direct3D 11 or modern OpenGL
5. G5 - only exists in Kinc
部分? kha targets 并不支持 graphics4,參考手冊中的 [Feature Matrix] 。