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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
|
..
---------------------------------------------------------------------------
Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies).
All rights reserved.
This work, unless otherwise expressly stated, is licensed under a
Creative Commons Attribution-ShareAlike 2.5.
The full license document is available from
http://creativecommons.org/licenses/by-sa/2.5/legalcode .
---------------------------------------------------------------------------
Shaders
=======
In the previous chapters, we have seen that we can define `Effects` with a texture and different material properties for our geometry in QML/3D. For most areas of applications, we do not need more than that, but if we want to achieve a custom effect, this technique has its limits. It is, however, possible in Qt Quick 2.0 and QML/3D to define custom effects as shader programs that allow you to extend the functionality of the built in effects.
The basics of shader programming are not covered in this guide. There are several tutorials out there, some of them even Qt specific. If you do not yet have any experience with shaders, we recommend that you first read the `OpenGL-Tutorial <http://qt.nokia.com/learning/guides>`_. In this section, we will only give you advice on how to use shaders in QML/3D and show you how to use them.
In QML/3D, shader programming is possible using the `ShaderProgram` element, which is derived from the more general `Effect` element. The `ShaderProgram` element has been extended by two properties: `fragmentShader` and `vertexShader`. Both take a string that contains the GLSL shader code.
Bulletshader
------------
The shader for the fired bullets will mix two textures, rotate them and adjust the interpolation level over time. The result should be the impression of a rotating and blinking object.
For rotation and interpolation, we are defining two new properties in the `ShaderProgram` element. A special feature of the `ShaderProgram` is the automatic property to uniform binding. This means that if we define a uniform variable in either of the shaders (fragment or vertex), the uniform is automatically bound to the `ShaderProgram's` property when they have the same name. The following code serves as an example:
.. code-block:: js
//Lasershader.qml
import QtQuick 2.0
import Qt3D 1.0
ShaderProgram {
blending: true
property real angle : 1.0
property real interpolationFactor : 1.0
property string texture2: "texture2.png"
...
fragmentShader: "
uniform mediump float angle;
uniform mediump float interpolationFactor;
uniform sampler2D texture2;
...
"
}
What you should also notice is that you can not only bind simple integer and float variables to uniforms, but also textures and matrices. Textures are therefore defined as string properties, however, the first texture can be defined using the `Effect's` `texture` property and is bound to the `qt_Texture0` uniform.
First we want to define the vertex shader, because it is a fairly simple task:
.. code-block:: js
//Lasershader.qml
ShaderProgram {
...
vertexShader: "
attribute highp vec4 qt_Vertex;
uniform mediump mat4 qt_ModelViewProjectionMatrix;
attribute highp vec4 qt_MultiTexCoord0;
varying mediump vec4 texCoord;
void main(void)
{
gl_Position = qt_ModelViewProjectionMatrix * qt_Vertex;
texCoord = qt_MultiTexCoord0;
}
"
}
There are the predefined attributes `qt_Vertex` (vertex position), `qt_MultiTexCoord0` (texture coordinates) of the currently processed vertex and the `qt_ModelViewProjectionMatrix`. We want to pass the texture coordinates to the next stage so we define a varying `texCoord` that we assign the texture coordinates to.
The fragment shader will be a bit more involved. There are two major tasks that we have to accomplish. Firstly, we need to rotate the texture according to the `angle` value and then we have to interpolate between the two `Sampler2Ds` that have been assigned to the shader. The rotation will be accomplished by rotating the texture coordinates with a 2D rotation matrix. Afterwards, we will be using the built in `mix` function in order to mix two color values and hand over the interpolation factor as a third parameter:
.. code-block:: js
//Lasershader.qml
ShaderProgram {
...
fragmentShader: "
varying highp vec4 texCoord;
uniform sampler2D qt_Texture0;
uniform sampler2D texture2;
uniform mediump float angle;
uniform mediump float interpolationFactor;
uniform mediump float hallo;
void main()
{
//The rotation matrix
mat2 RotationMatrix = mat2( cos( angle ), -sin( angle ),
sin( angle ), cos( angle ));
vec2 textureC = RotationMatrix*(texCoord.st-vec2(0.5))+vec2(0.5);
mediump vec4 texture1Color = texture2D(qt_Texture0, textureC);
mediump vec4 texture2Color = texture2D(texture2, textureC);
mediump vec4 textureColor = mix(texture1Color, texture2Color,
interpolationFactor);
gl_FragColor = textureColor;
}
"
}
Now we also want to animate the `interpolationFactor` and `angle` properties:
.. code-block:: js
//Lasershader.qml
ShaderProgram {
...
SequentialAnimation on interpolationFactor
{
running: true; loops: Animation.Infinite
NumberAnimation {
from: 0.3; to: 0.7;
duration: 800
}
PauseAnimation { duration: 200 }
NumberAnimation {
from: 0.7; to: 0.3;
duration: 800
}
PauseAnimation { duration: 500 }
}
NumberAnimation on angle{
from:0
to: Math.PI
duration: 1000;
running: true; loops: Animation.Infinite;
}
}
For enabling this `Effect` on our bullets, we have two options. The first option would be to directly assign the `Lasershader` to the `effect` property of the bullet, which would mean that whenever a new bullet is created, a new `ShaderProgram` is also created:
.. code-block:: js
//Bullet.qml
...
effect: Lasershader { }
...
The second option would be to create it globally in `game.qml` and assign the id of the effect to the bullet's `effect` property. The latter method saves more resources, but as you might notice, the `angle` and `interpolationFactor` stay the same for all bullets that are shot, and therefore, do not look as good as in the first method:
.. code-block:: js
//game.qml
...
Viewport {
...
Lasershader {id:bulleteffect}
}
.. code-block:: js
//Bullet.qml
...
Quad {
effect: bulleteffect
...
Explosion
---------
There are many ways to create explosions. Most of them, however, are quite difficult to implement. Our approach will be a very simple one, but quite aesthetic and realistic looking. We use the Billboarding* technique again and combine it with an animation. When an object explodes, one or more quads are created on which an explosion is shown that has been created before with a special program for example. In this context, *Animated* means that several pictures of an explosion are shown after each other (the same concept, as when watching a movie).
For a good explosion animation, we need at least 10 to 16 pictures to shown one after the other. We can, however, not include them separately in the vertex shader because we only have a certain amount of texture slots available on the graphic card. That is why we merge all explosion frames together into one big texture. This texture will be uploaded to the GPU and the fragment shader chooses which parts of the texture to use according to a time value. But first of all we create a new file called `Explosion.qml`. This will contain one `BillboardItem3D` that uses a quad as a mesh:
.. code-block:: js
//Explosion.qml
import QtQuick 2.0
import Qt3D 1.0
import Qt3D.Shapes 1.0
Quad{
id: explosionItem
scale:5
transform: [
Rotation3D{
angle: 90
axis: Qt.vector3d(1, 0, 0)
},
LookAt{ subject: camPos}
]
//wrapper around the camera position
Item3D { id: camPos
position: cam.eye
}
}
As already mentioned, we need a `lifetime` property for our explosion that has to be available in the fragment shader:
.. code-block:: js
//Explosion.qml
...
Quad{
...
NumberAnimation
{
running:true
target: program
property: "lifetime"
from: 0.0
to: 1.0;
duration: 1000
onRunningChanged: {
if(running==false)
explosionItem.enabled= false;
}
}
}
The `ShaderProgram` consists of the `lifetime` property used above, the `explo.png` texture, which has 16 explosion frames, a vertex and a fragment shader:
.. code-block:: js
//Explosion.qml
...
Quad{
...
effect: program
ShaderProgram {
id: program
texture: "explo.png"
property real lifetime : 1.0
blending: true
vertexShader: "
attribute highp vec4 qt_Vertex;
uniform mediump mat4 qt_ModelViewProjectionMatrix;
attribute highp vec4 qt_MultiTexCoord0;
uniform mediump float textureOffsetX;
varying mediump vec4 texCoord;
void main(void)
{
gl_Position = qt_ModelViewProjectionMatrix * qt_Vertex;
texCoord.st = qt_MultiTexCoord0.st;
}
"
...
}
}
The vertex shader is not really exciting because it just computes the position of the vertex and passes on the texture coordinates. The fragment shader, however, looks a bit more involved. We first multiply the `lifetime` by the number of frames we have in our texture and then try to find out which row and column position is the closest to our current `lifetime` value:
.. code-block:: js
//Explosion.qml
...
ShaderProgram{
...
fragmentShader: "
varying highp vec4 texCoord;
uniform sampler2D qt_Texture0;
uniform mediump float lifetime;
void main(void)
{
mediump int life = int(lifetime 16.0);
mediump int row = life % 4;
mediump int column = life / 4;
mediump vec4 textureColor = texture2D(qt_Texture0,
vec2(texCoord.s/4.0+0.25*float(row) ,
1.0-texCoord.t/4.0-0.25*float(column)));
gl_FragColor = textureColor;
}
"
}
Suitable animated explosions can be found everywhere on the internet. There is also software that can produce these textures from scratch. This technique is not only limited to displaying explosions. Thunderbolts and fire can also be animated.
The last thing needed for our explosion to work is the integration into our game. There are several ways of doing this. Either we define a global explosion which can be moved to the position of the exploding object or we implement the explosion in the objects. We now create a completely new component that can handle all possible explosions. For that the new component `ExplosionSystem.qml` is created:
.. code-block:: js
//ExplosionSystem.qml
import QtQuick 2.0
Timer {
id: explosion
running: true
property int loops: 40
property variant position: Qt.vector3d(0,0,0)
property variant explosionComponent: Qt.createComponent("Explosion.qml")
property real variation: 3
signal finished()
interval: 200
repeat: true
onTriggered: {
loops--
var object = explosionComponent.createObject(level,
{"x": position.x+(Math.random()-0.5) * variation,
"y": position.y+(Math.random()-0.5) * variation,
"z": position.z+(Math.random()-0.5) * variation})
if (loops==0) {
finished()
explosion.destroy()
}
}
}
.. Note:: Because we destroy the object after the `loops` property reaches `0`, the `ExplosionSystem` component may only be created dynamically with the `createObject` function.
The `ExplosionSystem` is created if the player has no hitpoints anymore:
.. code-block:: js
//Player.qml
...
Item3D {
...
function explode () {
root.state="EnterHighscore"
var component = Qt.createComponent("ExplosionSystem.qml")
var object = component.createObject(level, {"position": position})
object.finished.connect(enemy.exploded)
}
}
The same applies for the enemey:
.. code-block:: js
//Enemy.qml
...
Item3D {
...
function explode () {
root.score+=20
var component = Qt.createComponent("ExplosionSystem.qml")
var object = component.createObject(level, {"position": position})
object.finished.connect(enemy.exploded)
shootTimer.running=false
bossMovement.running=false
root.state="EnterHighscore"
}
}
.. rubric:: What's Next?
For this tutorial, this will be the final version of the game. Next we will however talk about how we can extend and improve it and give some ideas and instructions for further enhancement.
|