目录

(放个目录方便预览。知乎不支持目录,这个目录是从博客复制过来的,点击会跳转到博客)

  • 简介
  • 关于文章
  • 效果预览
    • 穿云洞
    • 星球之光
    • 蜗牛
    • 超级马里奥
  • 关于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
}

推荐阅读:

相关文章