玩转Qml(16)-移植ShaderToy
本文于
1006
天之前发表,文中内容可能已经过时。
简介 这次涛哥将会教大家移植ShaderToy的特效到Qml
源码 《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick
github https://github.com/jaredtao/TaoQuick
访问不了或者速度太慢,可以用国内的镜像网站gitee
https://gitee.com/jaredtao/TaoQuick
效果预览 先看几个效果图
穿云洞
星球之光
蜗牛
超级马里奥
gif录制质量较低,可编译运行TaoQuick源码或使用涛哥打包好的可执行程序,查看实际运行效果。
可执行程序下载链接(包括windows 和 MacOS平台) https://github.com/jaredtao/TaoQuick/releases
关于ShaderToy 学习过计算机图形学的人,都应该知道大名鼎鼎的ShaderToy网站
用一些Shader代码和简单的纹理,就可以输出各种酷炫的图形效果和音频效果。
如果你还不知道,赶紧去看看吧https://www.shadertoy.com
顺便提一下,该网站的作者是IQ大神,这里有他的博客:
http://www.iquilezles.org/www/articles/raymarchingdf/raymarchingdf.htm
本文主要讨论图形效果,音频效果以后再实现。
关于ShaderEffect Qml中实现ShaderToy,最快的途径就是ShaderEffect了。
上一篇文章《Qml特效-着色器效果ShaderEffect》已经介绍过ShaderEffect了, 本文重点是移植ShaderToy。
在涛哥写这篇文章之前,已经有两位前辈做过相关的研究。
陈锦明: https://zhuanlan.zhihu.com/p/38942460
qyvlik: https://zhuanlan.zhihu.com/p/44417680
涛哥参考了他们的实现,做了一些改进、完善。
在此感谢两位前辈。
下面正文开始
ShaderToy原理 OpenGL的可编程渲染管线中,着色器代码是可以动态编译、加载到GPU运行的。
而OpenGL又包括了桌面版(OpenGL Desktop)、嵌入式版(OpenGL ES)以及网页版(WebGL)
ShaderToy网站是以WebGL 2.0为基础,提供内置函数、变量,并约定了一些输入变量,由用户按照约定编写着色器代码。
只要不是太老的OpenGL版本,内置函数、变量基本都是通用的。
约定的变量 ShaderToy网站约定的变量如下:
1 2 3 4 5 6 7 8 9 10 11 vec3 iResolution image/buffer The viewport resolution (z is pixel aspect ratio, usually 1.0 )float iTime image/sound/buffer Current time in secondsfloat iTimeDelta image/buffer Time it takes to render a frame, in secondsint iFrame image/buffer Current framefloat iFrameRate image/buffer Number of frames rendered per secondfloat iChannelTime[4 ] image/buffer Time for channel (if video or sound), in secondsvec3 iChannelResolution[4 ] image/buffer /sound Input texture resolution for each channelvec4 iMouse image/buffer xy = current pixel coords (if LMB is down). zw = click pixelsampler2D iChannel{i} image/buffer /sound Sampler for input textures ivec4 iDate image/buffer /sound Year, month, day, time in seconds in .xyzwfloat iSampleRate image/buffer /sound The sound sample rate (typically 44100 )
Qml中的相应实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ShaderEffect { id: 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; } } 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 ; property var iChannel1 ; property var iChannel2 ; property var iChannel3 ; 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的运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 ShaderEffect { id: shader ... 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版本号 GLSL Versions
OpenGL Version
GLSL Version
2.0
110
2.1
120
3.0
130
3.1
140
3.2
150
3.3
330
4.0
400
4.1
410
4.2
420
4.3
430
GLSL ES Versions (Android, iOS, WebGL)
OpenGL ES Version
GLSL ES Version
2.0
100
3.0
300
glsl版本兼容 ShaderToy限定了WebGL 2.0,而我们移植到Qml中,自然是希望能够在所有可以运行Qml的设备上运行ShaderToy效果。
所以要做一些glsl版本相关的处理。
涛哥研究了Qt的GraphicsEffects模块源码,它的版本处理要么默认,要么 150 core,显然是不够用的。
glsl各个版本的差异,可以参考这里 https://github.com/mattdesl/lwjgl-basics/wiki/glsl-versions
涛哥总结出了如下的代码和注释说明:
注意”#version xxx”必须是着色器的第一行,不能换行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 readonly property string gles2Ver : " #define texture texture2D precision mediump float; " readonly property string gles3Ver : "#version 300 es #define varying in #define gl_FragColor fragColor precision mediump float; out vec4 fragColor; " readonly property string gl3Ver : "#version 150 #define varying in #define gl_FragColor fragColor #define lowp #define mediump #define highp out vec4 fragColor; " 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
1 2 3 4 5 6 7 8 9 10 11 12 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作为入口函数
1 2 3 4 5 6 7 8 9 10 11 12 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 最后,看一下代码的全貌吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 import QtQuick 2.12 import QtQuick.Controls 2.12 ShaderEffect { id: 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; } } 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 ; property var iChannel1 ; property var iChannel2 ; property var iChannel3 ; 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 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 } } readonly property string gles2Ver : " #define texture texture2D precision mediump float; " readonly property string gles3Ver : "#version 300 es #define varying in #define gl_FragColor fragColor precision mediump float; out vec4 fragColor; " readonly property string gl3Ver : "#version 150 #define varying in #define gl_FragColor fragColor #define lowp #define mediump #define highp out vec4 fragColor; " 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 }