Skip to content

Commit 06da732

Browse files
committed
docs: OpenGL objects
1 parent 41ba3e8 commit 06da732

File tree

3 files changed

+328
-3
lines changed

3 files changed

+328
-3
lines changed

README.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ straight forward ways to use VAOs, Shaders, Textures and FBOs.
2727
Documentation
2828
-------------
2929

30-
Project documentation can be found at readthedocs_. Optionally you can
31-
build your own docs from the ``docs`` directory.
30+
Project documentation can be found at readthedocs_. Optionally you can build your own
31+
docs from the ``docs`` directory. If anything is unclear or incorrect in the docs,
32+
please make an issue on it. This also applies when the docs are simply bad or
33+
too vague. Please also feel free to make pull requests on documentation.
3234

3335
Features
3436
--------

docs/source/effects.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,23 @@ The standard effect example:
119119
shader.uniform_mat3("m_normal", m_normal)
120120
self.cube.draw()
121121
122+
The parameters in the draw effect is:
123+
124+
- ``time``: The current time reported by our configured Timer in seconds.
125+
- ``frametime``: The time a frame is expected to take in seconds.
126+
This is useful when you cannot use ``time``. Should be avoided.
127+
- ``target`` is the target FBO of the effect
128+
129+
Time can potentially move at any speed or direction so it's good practice
130+
to make sure the effect can run when time is moving in any direction.
131+
132+
The ``bind_target`` decorator is useful when you want to ensure
133+
that an FBO passed to the effect is bound on entry and released on exit.
134+
By default a fake FBO is passed in representing the window framebuffer.
135+
EffectManagers can be used to pass in your own FBOs or another effect
136+
can call ``draw(..)`` requesting the result to end up in the FBO it passes in
137+
and then use this FBO as a texture on a cube.
138+
122139
Effect Base Class
123140
^^^^^^^^^^^^^^^^^
124141

docs/source/opengl.rst

Lines changed: 307 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,310 @@
22
OpenGL Objects
33
==============
44

5-
Write stuff..
5+
We proved some simple and powerful wrappers over OpenGL features.
6+
7+
- **Texture**: Textures from images or manually constructed/generated
8+
- **Shader**: Shader programs currently supporting vertex/fragment/geometry shaders
9+
- **Frame Buffer Object**: Offscreen rendering targets
10+
- **Vertex Array Object**: Represents the geometry we are drawing using a shader
11+
12+
Texture
13+
^^^^^^^
14+
15+
Textures are normally loaded by requesting the resource by path/name in the initializer
16+
of an effect using the ``self.get_texture`` method inherited from the ``Effect`` base class.
17+
18+
Textures can of course also be crated manually.
19+
20+
.. autoclass:: demosys.opengl.texture.Texture
21+
:members:
22+
:undoc-members:
23+
:show-inheritance:
24+
25+
Shader
26+
^^^^^^
27+
28+
In oder to draw something to the screen, we need a shader. There is no other way.
29+
30+
Shader should ideally always be loaded from ``.glsl`` files residing in a shader directory
31+
in your effect or project global resource directory. Shaders have to be written in a single
32+
file were the different shader types are separated using preprocessors.
33+
34+
Like textures they are loaded in the effect using the ``get_shader`` method in the initializer.
35+
36+
Once we have the reference to the shader object we will need a VAO object
37+
in order to bind it. We could of course just call ``bind()``, but **the VAOs
38+
will do this for you**. More details in the VAO section below.
39+
40+
.. code-block:: glsl
41+
42+
#version 410
43+
44+
#if defined VERTEX_SHADER
45+
// Vertex shader here
46+
#elif defined FRAGMENT_SHADER
47+
// Fragment shader here
48+
#elif defined GEOMETRY_SHADER
49+
// Geometry shader here
50+
#endif
51+
52+
Once the shader is bound we can set uniforms through the various ``uniform_`` methods.
53+
54+
Assuming we have a reference to a shader in ``s``:
55+
56+
.. code-block:: python
57+
58+
# Set the uniform (float) with name 'value' to 1.0
59+
s.uniform_1f("value", 1.0)
60+
# Set the uniform (mat4) with name `m_view' to a 4x4 matrix
61+
s.uniform_mat4("m_view", view_matrix)
62+
# Set the sampler2d uniform to use a Texture object we have loaded
63+
s.sampler_2d(0, "texture0", texture)
64+
65+
The Shader class contains an internal cache of all the uniform variables
66+
the shader has, so it will generally do very efficient type checks at run time
67+
and give useful error feedback if something is wrong.
68+
69+
Other than setting uniforms and using the right file format for shaders, there
70+
are not much more to them.
71+
72+
.. autoclass:: demosys.opengl.shader.Shader
73+
:members:
74+
:undoc-members:
75+
:show-inheritance:
76+
77+
Vertex Array Object
78+
^^^^^^^^^^^^^^^^^^^
79+
80+
Vertex Array Objects represents the geometry we are drawing with shaders.
81+
They keep track of the buffer binding states of one or multiple Vertex
82+
Buffer Objects.
83+
84+
VAOs and shaders interact in a very important way. The first time the VAO
85+
and shader interacts, they will figure out if they are compatible when it
86+
comes to the attributes in the shader and the buffers in the VAO.
87+
88+
When we create VAOs we tell explicitly what attribute name each buffer belongs to.
89+
90+
Example: I have three buffers representing positions, normals and uvs.
91+
92+
- Map positions to ``in_position`` attribute with 3 components
93+
- Map normals to ``in_normal`` attribute with 3 components
94+
- Map uvs to the ``in_uv`` attribute with 2 components
95+
96+
The vertex shader will have to define the exact same attribute names:
97+
98+
.. code-block:: glsl
99+
100+
in vec3 in_position;
101+
in vec3 in_normal;
102+
in vec2 in_uv
103+
104+
This is not entirely true. The shader will at least have to define
105+
the ``in_position``. The other two attributes are optional. This
106+
is were the VAO and the Shader object negotiates the attribute binding.
107+
The VAO object will on-the-fly generate a version of itself that
108+
supports the shaders attributes.
109+
110+
The VAO/Shader binding can also be used as a context manager as seen below,
111+
but this is optional. The context manager will return the reference to
112+
the shader so you can use a shorter name.
113+
114+
.. code-block:: python
115+
116+
# Bind the shader and negotiate attribute binding
117+
with vao.bind(shader) as s:
118+
s.unform_1f("value", 1.0)
119+
# ...
120+
# Finally draw the geometry
121+
vao.draw()
122+
123+
When creating a VBO we need to use the `OpenGL.arrays.vbo.VBO instance` in
124+
PyOpenGL. We pass a numpy array to the constructor. It's imporant to use
125+
the correct ``dtype`` so it matches the type passed in ``add_array_buffer``.
126+
127+
Each VBO is first added to the VAO using ``add_array_buffer``. This is simply
128+
to register the buffer and tell the VAO what format it has.
129+
130+
The ``map_buffer`` part will define the actual attribute mapping.
131+
Without this the VAO is not complete.
132+
133+
Calling ``build()`` will finalize and sanity check the VAO.
134+
135+
The VAO will always do **very** strict error checking and give
136+
useful feedback when something is wrong. VAOs must also be
137+
assigned a name so the framework can reference it in error messages.
138+
139+
.. code-block:: python
140+
141+
def quad_2d(width, height, xpos, ypos):
142+
"""
143+
Creates a 2D quad VAO using 2 triangles.
144+
145+
:param width: Width of the quad
146+
:param height: Height of the quad
147+
:param xpos: Center position x
148+
:param ypos: Center position y
149+
"""
150+
pos = VBO(numpy.array([
151+
xpos - width / 2.0, ypos + height / 2.0, 0.0,
152+
xpos - width / 2.0, ypos - height / 2.0, 0.0,
153+
xpos + width / 2.0, ypos - height / 2.0, 0.0,
154+
xpos - width / 2.0, ypos + height / 2.0, 0.0,
155+
xpos + width / 2.0, ypos - height / 2.0, 0.0,
156+
xpos + width / 2.0, ypos + height / 2.0, 0.0,
157+
], dtype=numpy.float32))
158+
normals = VBO(numpy.array([
159+
0.0, 0.0, 1.0,
160+
0.0, 0.0, 1.0,
161+
0.0, 0.0, 1.0,
162+
0.0, 0.0, 1.0,
163+
0.0, 0.0, 1.0,
164+
0.0, 0.0, 1.0,
165+
], dtype=numpy.float32))
166+
uvs = VBO(numpy.array([
167+
0.0, 1.0,
168+
0.0, 0.0,
169+
1.0, 0.0,
170+
0.0, 1.0,
171+
1.0, 0.0,
172+
1.0, 1.0,
173+
], dtype=numpy.float32))
174+
vao = VAO("geometry:quad", mode=GL.GL_TRIANGLES)
175+
vao.add_array_buffer(GL.GL_FLOAT, pos)
176+
vao.add_array_buffer(GL.GL_FLOAT, normals)
177+
vao.add_array_buffer(GL.GL_FLOAT, uvs)
178+
vao.map_buffer(pos, "in_position", 3)
179+
vao.map_buffer(normals, "in_normal", 3)
180+
vao.map_buffer(uvs, "in_uv", 2)
181+
vao.build()
182+
return vao
183+
184+
We can also pass index/element buffers to VAOs. We can also use
185+
interleaved VBOs by passing the same VBO to ``map_buffer`` multiple
186+
times.
187+
188+
More examples can be found in the :doc:`geometry` module.
189+
190+
.. autoclass:: demosys.opengl.vao.VAO
191+
:members:
192+
:undoc-members:
193+
:show-inheritance:
194+
195+
Frame Buffer Object
196+
^^^^^^^^^^^^^^^^^^^
197+
198+
Frame Buffer Objects are offscreen render targets.
199+
Internally they are simply textures that can be used further in rendering.
200+
FBOs can even have multiple layers so a shader can write to multiple buffers at once.
201+
They can also have depth buffers and stencil buffers. Currently we by default
202+
use a depth 24 / stencil 8 buffer as the depth format.
203+
204+
Creating an FBO:
205+
206+
.. code-block:: python
207+
208+
# Shorcut for creating a single layer FBO with depth buffer
209+
fbo = FBO.create(1024, 1024, depth=True)
210+
211+
# Multilayer FBO (We really need to make a shortcut for this!)
212+
fbo = FBO()
213+
fbo.add_color_attachment(texture1)
214+
fbo.add_color_attachment(texture2)
215+
fbo.add_color_attachment(texture3)
216+
fbo.set_depth_attachment(depth_texture)
217+
218+
# Binding and releasing FBOs
219+
fbo.bind()
220+
fbo.release()
221+
222+
# Using a context manager
223+
with fbo:
224+
# Draw stuff in the FBO
225+
226+
When binding the FBOs with multiple color attachments it will automatically
227+
call ``glDrawBuffers`` enabling multiple outputs in the framgment shader.
228+
229+
Shader example with multiple layers:
230+
231+
.. code-block:: glsl
232+
233+
#version 410
234+
235+
layout(location = 0) out vec4 outColor0;
236+
layout(location = 1) out vec4 outColor1;
237+
layout(location = 2) out vec4 outColor2;
238+
239+
void main( void ) {
240+
outColor0 = vec4(1.0, 0.0, 0.0, 1.0)
241+
outColor1 = vec4(0.0, 1.0, 0.0, 1.0)
242+
outColor1 = vec4(0.0, 0.0, 1.0, 1.0)
243+
}
244+
245+
Will draw red, green and blue in the separate layers in the FBO.
246+
247+
.. Warning:: It's important to use explicit attribute locations as not all drivers
248+
will guarantee preservation of the order and things end up in the wrong buffers!
249+
250+
Another **very important** feature of the FBO implementation is
251+
the concept of FBO stacks.
252+
253+
- The default render target is the window frame buffer.
254+
- When the stack is empty we are rendering to the screen.
255+
- When binding an FBO it will be pushed to the stack and the correct viewport
256+
for the FBO will be set
257+
- When releasing the FBO it will be popped from the stack and
258+
the viewport for the default render target will be applied
259+
- This also means we can build deeper stacks with the same behavior
260+
- The maximum stack depth is currently 8 and the framework will
261+
aggressively react when FBOs are popped and pushed in the wrong order
262+
263+
A more complex example:
264+
265+
.. code-block:: python
266+
267+
# Push fbo1 to stack, bind and set viewport
268+
fbo1.bind()
269+
# Push fbo2 to stack, bind and set viewport
270+
fbo2.bind()
271+
# Push fbo3 to stack, bind and set viewport
272+
fbo3.bind()
273+
# Pop fbo3 from stack, bind fbo2 and set the viewport
274+
fbo3.release()
275+
# Pop fbo2 from stack, bind fbo1 and set the viewport
276+
fbo2.release()
277+
# Pop fbo1 from stack, unbind the fbo and set the screen viewport
278+
fbo1.release()
279+
280+
Using context managers:
281+
282+
.. code-block:: python
283+
284+
with fbo1:
285+
with fbo2:
286+
with fbo2:
287+
pass
288+
289+
This is especially useful in realation to the ``draw`` method in effects.
290+
The last parameter is the target FBO. The effect will never know if the
291+
FBO passed in is the fake window FBO or an actual FBO. It might also
292+
do offscreen rendering to its own fbos and things start get get really ugly.
293+
294+
The FBO stack makes this fairly painless.
295+
296+
By using the ``bind_target`` decorator on the ``draw`` method of your effect
297+
you will never need to think about this issue. Not having to worry about
298+
resporting the viewport size is also a huge burden off our shoulders.
299+
300+
.. code-block:: python
301+
302+
@effect.bind_target
303+
def draw(self, time, frametime, target):
304+
# ...
305+
306+
There are of course ways to bypass the stack, but should be done with extreme caution.
307+
308+
.. autoclass:: demosys.opengl.fbo.FBO
309+
:members:
310+
:undoc-members:
311+
:show-inheritance:

0 commit comments

Comments
 (0)