博客 / 詳情

返回

使用 C++ 模擬 ShaderLanguage 的 swizzle

swizzle 語法

經常編寫着色器的同學應該對 swizzle(重排)語法非常熟悉,方便又靈活,可以説是用過一次便回味無窮。

代碼

vec4 color = vec4(1.0, 0.5, 0.0, 1.0);
vec3 rgb = color.rgb;        // { 1.0, 0.5, 0.0 }
vec2 xy = color.xy;          // { 1.0, 0.5 }
vec4 bgra = color.bgra;      // { 0.0, 0.5, 1.0, 1.0 }

可惜的是,C++ 中並不存在這樣的語法,但是可以利用語法特性來模擬它,基本的思路是使用一個代理類來存儲被操作點的引用以及需要操作的位置信息。

知名的 swizzle 實現

GLM

作為圖形編程中的常客,GLM 提供了一套和 GLSL 相似的 swizzle 語法,只需要在使用前定義宏 GLM_FORCE_SWIZZLE 即可在向量類中使用了:

代碼

#define GLM_FORCE_SWIZZLE
#include <glm/glm.hpp>
glm::vec3 v{1.0f, 2.0f, 3.0f}; v.xy = v.yz; glm::vec3 reverse = v.zyx;

GLM 的實現方式是在類的未命名 union 內部定義一系列預定義的 swizzle 組合代理類,這些類只存儲一個標記 vec 類內存起始位置的 char _buffer[1],而需要操作的位置信息則以模板參數形式編譯進類型信息本身。

當一個 vec 類被構造時,這些代理類的 _buffer 即被初始化為 vec 實例的內存起始位置,當需要訪問代理類的數據時,將 _buffer 轉換為 vec 實例化時的數值類型指針,再取出位置信息作為索引即可實現對 vec 數據進行特定模式的訪問。

GLM 的 swizzle 實現可以説是非常優雅,在形式和作用上是最還原 GLSL swizzle 語法的。

然而這種實現方式有一個缺點:所有的 swizzle 組合都是預定義的。GLM 的 vec 支持 2,3,4 維度的 swizzle,以 glm::vec3 來舉例,它有 3 個元素,則能夠組成的 swizzle 組合的總數為:

$$ \begin{aligned} N=\sum_{i=2}^{4} 3^i=117 \\ \end{aligned} $$

也就是説在 glm::vec3 的類定義中會有 117 個類似於 xx, xy, xxx, xyz, xxxx, zyzw 這樣的成員(位於未命名 union 內)。雖然它們共用同一塊內存,不會增加類的大小,但是代碼編輯器的智能補全會將它們一一列舉出來,這會讓其他的成員變量、函數淹沒在這些符號之間,體驗上多少有點不好:

注意到 GLM 的絕大多數向量的計算操作都是使用外部函數例如 glm::normalize(v); 而沒有將它們寫成成員函數,是否也跟這個問題有點關係?

Eigen

Eigen 並沒有直接提供 swizzle 語法,但是它的 IndexedView 提供類似的功能:

代碼

Eigen::Vector3f v{ 1.0f, 2.0f, 3.0f };
//swz 類型是 Eigen::IndexedView<Eigen::Vector3f, Eigen::Array<int, 2, 1>, Eigen::internal::SingleRange<0>>
auto swz = v({1, 0});
swz = Eigen::Vector2f{4.0f, 5.0f}; 
v({0, 1, 2}) = v({2, 0, 1}); //相當於 v.xyz = v.zxy

我沒有細看 Eigen 的源碼,但是表面上猜測,IndexedView 類的實現思路基本上也是一種代理的思想,並且它應該將綁定數據的引用和位置信息都保存在了類的數據成員中:

代碼

Eigen::Vector3f v{ 1.0f, 2.0f, 3.0f };
constexpr int swz2_size = sizeof(v({0, 1}));                //24 byte
constexpr int swz3_size = sizeof(v({0, 1, 2}));             //24 byte
constexpr int swz4_size = sizeof(v({0, 0, 1, 2}));          //48 byte
constexpr int swz5_size = sizeof(v({0, 0, 1, 1, 2}));       //32 byte
constexpr int swz6_size = sizeof(v({0, 0, 1, 1, 2, 2}));    //40 byte

不同長度的 IndexedView 類的大小是不同的,這説明 IndexedView 類確實將位置信息也保存成為了數據成員。可以看到不同的長度對應的類大小增長很符合 8 字節對齊的特徵,但有趣的是長度為 4 時比較反常,經過我的實驗,長度為 4 的倍數的 IndexedView 的大小都比較反常,估計是 Eigen 內部的針對性優化導致的。

總的來説,Eigen 的 IndexedView 完全可以滿足 swizzle 的功能,但它的主要目標是通用和高效,沒有必要為特定的語法作封裝。

我的實現

我在編寫 point 類時並不知道 GLM 的 swizzle 模塊,更不知道 Eigen 的 IndexedView,但是最終實現出來的代碼用的思路都相同:用一個代理類作為中間層來進行數據的間接訪問。

代理類 exchanger 將位置編譯進類型信息中,並保存一個操作數據對象的指針,通過自定義的賦值運算符和類型轉換運算符與其他的數據類型進行數據交換,邏輯相當簡單。

為了實現使用 xyzw, rgba, stpq 這些標籤指定位置信息,我借用 boost preprocessor 庫修改每個 swizzle 函數的調用參數為它對應的 index。對於左值操作對象,swizzle 返回一個 exchanger(或 const_exchanger) 類的實例,而如果操作對象是右值,則直接返回一個對應長度的 point 實例(數據拷貝而非引用,避免野指針)。

最終實現的 point 類支持任意長度(實際受限於 boost preprocessor 和編譯器限制)和任意位置(代碼中支持 0-255,但可以通過在 point_swizzler.hpp 的 POINT_SWIZZLE_CONVERT_PREFIX_255 之後繼續添加條目支持更多的位置)的 swizzle.

代碼在這裏,可以在 test.cpp 中查看使用示例,目前只是提供一種 swizzle 的實現,尚未經過嚴格測試。

總結

在實現完 point 類之後,我才發現 GLM 和 Eigen 中的類似功能實現,又是一次重複造輪子。但是還是頗有收穫的,想當初入門 C++ 時看到模板代碼就頭疼,現在不管多複雜的庫代碼也能慢慢剖開分析實現思路,其中很多技巧都是在一次次造輪子中深入掌握的。

總結一下各個實現版本的特點吧:

GLM: 完美還原着色器的 swizzle 語法,但組合是固定的;

Eigen: 支持任意的重排操作,但沒有語法上的封裝;

我的實現:支持任意的重排操作,但沒有完全還原着色器語法。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.