目錄
(放個目錄方便預覽。知乎不支持目錄,這個目錄是從博客複製過來的,點擊會跳轉到博客)
這是《Qml特效》系列文章的特別篇,濤哥將會教大家移植ShaderToy的特效到Qml。
文章主要發布在濤哥的博客 和 濤哥的知乎專欄-Qt進階之路
先看幾個效果圖
gif錄製質量較低,可編譯運行源碼或使用濤哥打包好的可執行程序,查看實際運行效果。
源碼倉庫1 https://github.com/jaredtao/TaoQuick
源碼倉庫2 https://github.com/jaredtao/TaoShaderToy
可執行程序下載鏈接(包括windows 和 MacOS平臺) https://github.com/jaredtao/TaoQuick/releases
學習過計算機圖形學的人,都應該知道大名鼎鼎的ShaderToy網站
用一些Shader代碼和簡單的紋理,就可以輸出各種酷炫的圖形效果和音頻效果。
如果你還不知道,趕緊去看看吧https://www.shadertoy.com
順便提一下,該網站的作者是IQ大神,這裡有他的博客:
http://www.iquilezles.org/www/articles/raymarchingdf/raymarchingdf.htm
本文主要討論圖形效果,音頻效果以後再實現。
Qml中實現ShaderToy,最快的途徑就是ShaderEffect了。
上一篇文章《Qml特效-著色器效果ShaderEffect》已經介紹過ShaderEffect了, 本文重點是移植ShaderToy。
在濤哥寫這篇文章之前,已經有兩位前輩做過相關的研究。
陳錦明: https://zhuanlan.zhihu.com/p/38942460
qyvlik: https://zhuanlan.zhihu.com/p/44417680
濤哥參考了他們的實現,做了一些改進、完善。
在此感謝兩位前輩。
下面正文開始
OpenGL的可編程渲染管線中,著色器代碼是可以動態編譯、載入到GPU運行的。
而OpenGL又包括了桌面版(OpenGL Desktop)、嵌入式版(OpenGL ES)以及網頁版(WebGL)
ShaderToy網站是以WebGL 2.0為基礎,提供內置函數、變數,並約定了一些輸入變數,由用戶按照約定編寫著色器代碼。
只要不是太老的OpenGL版本,內置函數、變數基本都是通用的。
ShaderToy網站約定的變數如下:
vec3 iResolution image/buffer The viewport resolution (z is pixel aspect ratio, usually 1.0) float iTime image/sound/buffer Current time in seconds float iTimeDelta image/buffer Time it takes to render a frame, in seconds int iFrame image/buffer Current frame float iFrameRate image/buffer Number of frames rendered per second float iChannelTime[4] image/buffer Time for channel (if video or sound), in seconds vec3 iChannelResolution[4] image/buffer/sound Input texture resolution for each channel vec4 iMouse image/buffer xy = current pixel coords (if LMB is down). zw = click pixel sampler2D iChannel{i} image/buffer/sound Sampler for input textures i vec4 iDate image/buffer/sound Year, month, day, time in seconds in .xyzw float iSampleRate image/buffer/sound The sound sample rate (typically 44100)
Qml中的相應實現
ShaderEffect { id: shader
//properties for shader
//not pass to shader readonly property vector3d defaultResolution: Qt.vector3d(shader.width, shader.height, shader.width / shader.height) function calcResolution(channel) { if (channel) { return Qt.vector3d(channel.width, channel.height, channel.width / channel.height); } else { return defaultResolution; } } //pass readonly property vector3d iResolution: defaultResolution property real iTime: 0 property real iTimeDelta: 100 property int iFrame: 10 property real iFrameRate property vector4d iMouse; property var iChannel0; //only Image or ShaderEffectSource property var iChannel1; //only Image or ShaderEffectSource property var iChannel2; //only Image or ShaderEffectSource property var iChannel3; //only Image or ShaderEffectSource property var iChannelTime: [0, 1, 2, 3] property var iChannelResolution: [calcResolution(iChannel0), calcResolution(iChannel1), calcResolution(iChannel2), calcResolution(iChannel3)] property vector4d iDate; property real iSampleRate: 44100
...
}
其中時間、日期通過Timer刷新,滑鼠位置用MouseArea刷新。
同時濤哥導出了hoverEnabled、running屬性和restart函數,以方便Qml中控制Shader的運行。
ShaderEffect { id: shader ... //properties for Qml controller property alias hoverEnabled: mouse.hoverEnabled property bool running: true function restart() { shader.iTime = 0 running = true timer1.restart() }
Timer { id: timer1 running: shader.running triggeredOnStart: true interval: 16 repeat: true onTriggered: { shader.iTime += 0.016; } } Timer { running: shader.running interval: 1000 onTriggered: { var date = new Date(); shader.iDate.x = date.getFullYear(); shader.iDate.y = date.getMonth(); shader.iDate.z = date.getDay(); shader.iDate.w = date.getSeconds() } } MouseArea { id: mouse anchors.fill: parent onPositionChanged: { shader.iMouse.x = mouseX shader.iMouse.y = mouseY } onClicked: { shader.iMouse.z = mouseX shader.iMouse.w = mouseY } } ... }
ShaderToy限定了WebGL 2.0,而我們移植到Qml中,自然是希望能夠在所有可以運行Qml的設備上運行ShaderToy效果。
所以要做一些glsl版本相關的處理。
濤哥研究了Qt的GraphicsEffects模塊源碼,它的版本處理要麼默認,要麼 150 core,顯然是不夠用的。
glsl各個版本的差異,可以參考這裡 https://github.com/mattdesl/lwjgl-basics/wiki/glsl-versions
濤哥總結出了如下的代碼和注釋說明:
注意」#version xxx」必須是著色器的第一行,不能換行
// 如果環境是OpenGL ES2,默認的version是 version 110, 不需要寫出來。 // 比ES2更老的版本是ES 1.0 和 ES 1.1, 這種古董設備,建議還是不要玩Shader了吧。 // ES2沒有texture函數,要用舊的texture2D代替 // 精度限定要寫成float
readonly property string gles2Ver: " #define texture texture2D precision mediump float; " // 如果環境是OpenGL ES3,version是 version 300 es // ES 3.1 ES 3.2也可以。 // ES3可以用in out 關鍵字,gl_FragColor也可以用out fragColor取代 // 精度限定要寫成float
readonly property string gles3Ver: "#version 300 es #define varying in #define gl_FragColor fragColor precision mediump float;
out vec4 fragColor; " // 如果環境是OpenGL Desktop 3.x,version這裡參考Qt默認的version 150。大部分Desktop設備應該 // 都是150, 即3.2版本,第一個區分Core和Compatibility的版本。 // Core是核心模式,只有核心api以減輕負擔。相應的Compatibility是兼容模式,保留全部API以兼容低版本。 // Desktop 3.x 可以用in out 關鍵字,gl_FragColor也可以用out fragColor取代 // 精度限定抹掉,用默認的。不抹掉有些情況下會報錯,不能通用。 readonly property string gl3Ver: "#version 150 #define varying in #define gl_FragColor fragColor #define lowp #define mediump #define highp
out vec4 fragColor; " // 如果環境是OpenGL Desktop 2.x,version這裡就用2.0的version 110,即2.0版本 // 2.x 沒有texture函數,要用舊的texture2D代替 readonly property string gl2Ver: "#version 110 #define texture texture2D " property string versionString: { if (Qt.platform.os === "android") { if (GraphicsInfo.majorVersion === 3) { console.log("android gles 3") return gles3Ver } else { console.log("android gles 2") return gles2Ver } } else { if (GraphicsInfo.majorVersion === 3 ||GraphicsInfo.majorVersion === 4) { return gl3Ver } else { return gl2Ver } } } readonly property string forwardString: versionString + " varying vec2 qt_TexCoord0; varying vec4 vertex; uniform lowp float qt_Opacity;
uniform vec3 iResolution; uniform float iTime; uniform float iTimeDelta; uniform int iFrame; uniform float iFrameRate; uniform float iChannelTime[4]; uniform vec3 iChannelResolution[4]; uniform vec4 iMouse; uniform vec4 iDate; uniform float iSampleRate; uniform sampler2D iChannel0; uniform sampler2D iChannel1; uniform sampler2D iChannel2; uniform sampler2D iChannel3; "
versionString 這裡,主要測試了Desktop和 android設備,Desktop只要顯卡不太搓,都能運行的。
Android ES3的也是全部支持,ES2的部分不能運行,比如iq大神的蝸牛Shader,使用了textureLod等一系列內置函數,就不能在ES2上面跑。
本來是不需要寫頂點著色器的。如果我們想把ShaderToy做成一個任意坐標開始的Item來用,就需要適配一下坐標。
濤哥寫的頂點著色器如下,僅在默認著色器的基礎上,傳遞qt_Vertex給下一階段的vertex
vertexShader: " uniform mat4 qt_Matrix; attribute vec4 qt_Vertex; attribute vec2 qt_MultiTexCoord0; varying vec2 qt_TexCoord0; varying vec4 vertex; void main() { vertex = qt_Vertex; gl_Position = qt_Matrix * vertex; qt_TexCoord0 = qt_MultiTexCoord0; }"
片段著色器這裡處理一下,適配出一個符合shaderToy的mainImage作為入口函數
readonly property string startCode: " void main(void) { mainImage(gl_FragColor, vec2(vertex.x, iResolution.y - vertex.y)); }" readonly property string defaultPixelShader: " void mainImage(out vec4 fragColor, in vec2 fragCoord) { fragColor = vec4(fragCoord, fragCoord.x, fragCoord.y); }" property string pixelShader: "" fragmentShader: forwardString + (pixelShader ? pixelShader : defaultPixelShader) + startCode
稍微說明一下,qyvlik大佬的Shader使用gl_FragCoord作為片段坐標傳進去了,這種用法的ShaderToy坐標將會佔據整個Qml的窗口,
而實際ShaderToy坐標不是整個窗口的時候,超出去的地方就會被切掉,顯示出來的只有一小部分。
濤哥研究了一番後,頂點著色器把vertex傳過來,vertex.x就是x坐標,vertex.y坐標從上到下是0 - height,而gl_FragCoord 從下到上是0 - height,
所以要翻一下。
最後,看一下代碼的全貌吧
//TaoShaderToy.qml import QtQuick 2.12 import QtQuick.Controls 2.12 /* vec3 iResolution image/buffer The viewport resolution (z is pixel aspect ratio, usually 1.0) float iTime image/sound/buffer Current time in seconds float iTimeDelta image/buffer Time it takes to render a frame, in seconds int iFrame image/buffer Current frame float iFrameRate image/buffer Number of frames rendered per second float iChannelTime[4] image/buffer Time for channel (if video or sound), in seconds vec3 iChannelResolution[4] image/buffer/sound Input texture resolution for each channel vec4 iMouse image/buffer xy = current pixel coords (if LMB is down). zw = click pixel sampler2D iChannel{i} image/buffer/sound Sampler for input textures i vec4 iDate image/buffer/sound Year, month, day, time in seconds in .xyzw float iSampleRate image/buffer/sound The sound sample rate (typically 44100) */ ShaderEffect { id: shader
//properties for Qml controller property alias hoverEnabled: mouse.hoverEnabled property bool running: true function restart() { shader.iTime = 0 running = true timer1.restart() } Timer { id: timer1 running: shader.running triggeredOnStart: true interval: 16 repeat: true onTriggered: { shader.iTime += 0.016; } } Timer { running: shader.running interval: 1000 onTriggered: { var date = new Date(); shader.iDate.x = date.getFullYear(); shader.iDate.y = date.getMonth(); shader.iDate.z = date.getDay(); shader.iDate.w = date.getSeconds() } } MouseArea { id: mouse anchors.fill: parent onPositionChanged: { shader.iMouse.x = mouseX shader.iMouse.y = mouseY } onClicked: { shader.iMouse.z = mouseX shader.iMouse.w = mouseY } } // 如果環境是OpenGL ES2,默認的version是 version 110, 不需要寫出來。 // 比ES2更老的版本是ES 1.0 和 ES 1.1, 這種古董設備,還是不要玩Shader了吧。 // ES2沒有texture函數,要用舊的texture2D代替 // 精度限定要寫成float readonly property string gles2Ver: " #define texture texture2D precision mediump float; " // 如果環境是OpenGL ES3,version是 version 300 es // ES 3.1 ES 3.2也可以。 // ES3可以用in out 關鍵字,gl_FragColor也可以用out fragColor取代 // 精度限定要寫成float readonly property string gles3Ver: "#version 300 es #define varying in #define gl_FragColor fragColor precision mediump float;
out vec4 fragColor; " // 如果環境是OpenGL Desktop 3.x,version這裡參考Qt默認的version 150。大部分Desktop設備應該都是150 // 150 即3.2版本,第一個區分Core和Compatibility的版本。Core是核心模式,只有核心api以減輕負擔。相應的Compatibility是兼容模式,保留全部API以兼容低版本。 // 可以用in out 關鍵字,gl_FragColor也可以用out fragColor取代 // 精度限定抹掉,用默認的。不抹掉有些情況下會報錯,不能通用。 readonly property string gl3Ver: "#version 150 #define varying in #define gl_FragColor fragColor #define lowp #define mediump #define highp
out vec4 fragColor; " // 如果環境是OpenGL Desktop 2.x,version這裡就用2.0的version 110,即2.0版本 // 2.x 沒有texture函數,要用舊的texture2D代替 readonly property string gl2Ver: "#version 110 #define texture texture2D "
property string versionString: { if (Qt.platform.os === "android") { if (GraphicsInfo.majorVersion === 3) { console.log("android gles 3") return gles3Ver } else { console.log("android gles 2") return gles2Ver } } else { if (GraphicsInfo.majorVersion === 3 ||GraphicsInfo.majorVersion === 4) { return gl3Ver } else { return gl2Ver } } }
vertexShader: " uniform mat4 qt_Matrix; attribute vec4 qt_Vertex; attribute vec2 qt_MultiTexCoord0; varying vec2 qt_TexCoord0; varying vec4 vertex; void main() { vertex = qt_Vertex; gl_Position = qt_Matrix * vertex; qt_TexCoord0 = qt_MultiTexCoord0; }" readonly property string forwardString: versionString + " varying vec2 qt_TexCoord0; varying vec4 vertex; uniform lowp float qt_Opacity;
uniform vec3 iResolution; uniform float iTime; uniform float iTimeDelta; uniform int iFrame; uniform float iFrameRate; uniform float iChannelTime[4]; uniform vec3 iChannelResolution[4]; uniform vec4 iMouse; uniform vec4 iDate; uniform float iSampleRate; uniform sampler2D iChannel0; uniform sampler2D iChannel1; uniform sampler2D iChannel2; uniform sampler2D iChannel3; " readonly property string startCode: " void main(void) { mainImage(gl_FragColor, vec2(vertex.x, iResolution.y - vertex.y)); }" readonly property string defaultPixelShader: " void mainImage(out vec4 fragColor, in vec2 fragCoord) { fragColor = vec4(fragCoord, fragCoord.x, fragCoord.y); }" property string pixelShader: "" fragmentShader: forwardString + (pixelShader ? pixelShader : defaultPixelShader) + startCode }
推薦閱讀: