目錄

(放個目錄方便預覽。知乎不支持目錄,這個目錄是從博客複製過來的,點擊會跳轉到博客)

  • 簡介
  • 關於文章
  • 效果預覽
    • 穿雲洞
    • 星球之光
    • 蝸牛
    • 超級馬裏奧
  • 關於ShaderToy
  • 關於ShaderEffect
  • ShaderToy原理
    • 約定的變數
    • glsl版本兼容
    • ShaderToy適配
  • TaoShaderToy

簡介

這是《Qml特效》系列文章的特別篇,濤哥將會教大家移植ShaderToy的特效到Qml。

關於文章

文章主要發布在濤哥的博客 和 濤哥的知乎專欄-Qt進階之路

效果預覽

先看幾個效果圖

穿雲洞

星球之光

蝸牛

超級馬裏奧

gif錄製質量較低,可編譯運行源碼或使用濤哥打包好的可執行程序,查看實際運行效果。

源碼倉庫1 github.com/jaredtao/Tao

源碼倉庫2 github.com/jaredtao/Tao

可執行程序下載鏈接(包括windows 和 MacOS平臺) github.com/jaredtao/Tao

關於ShaderToy

學習過計算機圖形學的人,都應該知道大名鼎鼎的ShaderToy網站

用一些Shader代碼和簡單的紋理,就可以輸出各種酷炫的圖形效果和音頻效果。

如果你還不知道,趕緊去看看吧https://www.shadertoy.com

順便提一下,該網站的作者是IQ大神,這裡有他的博客:

iquilezles.org/www/arti

本文主要討論圖形效果,音頻效果以後再實現。

關於ShaderEffect

Qml中實現ShaderToy,最快的途徑就是ShaderEffect了。

上一篇文章《Qml特效-著色器效果ShaderEffect》已經介紹過ShaderEffect了, 本文重點是移植ShaderToy。

在濤哥寫這篇文章之前,已經有兩位前輩做過相關的研究。

陳錦明: zhuanlan.zhihu.com/p/38

qyvlik: zhuanlan.zhihu.com/p/44

濤哥參考了他們的實現,做了一些改進、完善。

在此感謝兩位前輩。

下面正文開始

ShaderToy原理

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
}
}
...
}

glsl版本兼容

ShaderToy限定了WebGL 2.0,而我們移植到Qml中,自然是希望能夠在所有可以運行Qml的設備上運行ShaderToy效果。

所以要做一些glsl版本相關的處理。

濤哥研究了Qt的GraphicsEffects模塊源碼,它的版本處理要麼默認,要麼 150 core,顯然是不夠用的。

glsl各個版本的差異,可以參考這裡 github.com/mattdesl/lwj

濤哥總結出了如下的代碼和注釋說明:

注意」#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適配

本來是不需要寫頂點著色器的。如果我們想把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

最後,看一下代碼的全貌吧

//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 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

//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
}

推薦閱讀:

相關文章