目录
(放个目录方便预览。知乎不支持目录,这个目录是从博客复制过来的,点击会跳转到博客)
这是《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 }
推荐阅读: