Introduction

This module is exposing a part of the WebGL2 context. Is is assumed that you are familiar with the concepts and commands. You can find more information about it here : https://webgl2fundamentals.org/

There is some major differences still :

  • All the WebGL2 commands are called on the GLViewer instead of a gl context.

  • All the API is written in snake_case instead of camelCase, so for example gl.drawArrays(...) in JavaScript becomes widget.draw_arrays(...) in Python

  • Masks parameters are replaced by positional attribute, so for example gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT); in JavaScript becomes widget.clear(depth_buffer_bit=True, color_buffer_bit=True) in Python

  • Enums are replaced by strings, so for example gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW); in JavaScript becomes widget.buffer_data("ARRAY_BUFFER", data, "DYNAMIC_DRAW") in Python

  • There is no delete functions, once something is created it stays created (we are in a prototype environment).

  • You will find some ‘Extended’ methods that can simplify some calls like the create_vertex_array_ext that will create and link the programs and buffers all at once.

Not all the functions are exposed as of today. If you need more, feel free to ask on github https://github.com/JeromeEippers/ipywebgl.

All the commands you call on the GLViewer are push to a commands buffer. That commands buffer is only flushed when you call the execute_commands() method.

Very first triangle

Let’s create a simple webgl renderer with a single triangle in it.

Import

Import the ipywebgl module and numpy

[1]:
import ipywebgl
import numpy as np

Viewer

Create an instance of the viewer and change the clear color (to see the canvas) and render that change.

[2]:
w = ipywebgl.GLViewer()
w.clear_color(.8, .8, .8 ,1)
w.clear()
w.execute_commands()
w
[2]:

Program

Create the simple program to display a triangle in clip space

For this we will : * create a vertex shader * create a fragment shader * create a program * attach the shaders to the program * and link the program

[3]:
vertex_shader = w.create_shader('VERTEX_SHADER')
w.shader_source(vertex_shader,
"""#version 300 es

in vec4 in_position;

void main() {
  gl_Position = in_position;
}
""")
w.compile_shader(vertex_shader)

# execute commands so we can see if our shader compiled
w.execute_commands(execute_once=True)

#display the resource
vertex_shader
[3]:
[4]:
fragment_shader = w.create_shader('FRAGMENT_SHADER')
w.shader_source(fragment_shader,
"""#version 300 es
precision highp float;

out vec4 outColor;

void main() {
  outColor = vec4(1, 0, 0.5, 1);
}
""")
w.compile_shader(fragment_shader)

# execute commands so we can see if our shader compiled
w.execute_commands(execute_once=True)

#display the resource
fragment_shader
[4]:
[5]:
program = w.create_program()
w.attach_shader(program, vertex_shader)
w.attach_shader(program, fragment_shader)
w.link_program(program)

# execute commands so we can see if our program links
w.execute_commands(execute_once=True)

#display the resource
program
[5]:

Buffer

Create a buffer to store the vertices values and fill it with 2d positions.

The buffer has 3 vertices with X Y values

[6]:
vbo = w.create_buffer()

# bind the buffer and set the data in it
w.bind_buffer('ARRAY_BUFFER', vbo)
w.buffer_data(
    'ARRAY_BUFFER',
    np.array(
        [ 0, 0,
          0, 0.5,
          0.7, 0,
        ], dtype=np.float32),
    'STATIC_DRAW',
    update_info=True)

# execute commands so we can see if the buffer is updated
w.execute_commands(execute_once=True)

#display the resource
vbo
[6]:

Vertex Array

Create a vertex array and bind the program and the buffer.

[7]:
vao = w.create_vertex_array()
w.bind_vertex_array(vao)

# use the program so we can find the attributes
w.use_program(program)
# bind the vertex buffer we want to use in this vertex array
w.bind_buffer('ARRAY_BUFFER', vbo)
# enable and set the pointer to the attribute in the vertex array
w.enable_vertex_attrib_array('in_position')
w.vertex_attrib_pointer('in_position', 2, "FLOAT", False, 2*4, 0)

# execute commands so we can see if the buffer is updated
w.execute_commands(execute_once=True)

#display the resource
vao
[7]:

Draw

Update the commands buffer to render that triangle, and call render to send it to the frontend

[8]:
w.clear()

w.use_program(program)
w.bind_vertex_array(vao)
w.draw_arrays('TRIANGLES', 0, 3)

# render in loop if needed
w.execute_commands()

Move the GLViewer

By redisplaying the viewer here we move it down here.

[13]:
w
[13]:

Uniform

We will update the shader to use an uniform to move the triangle around.

[10]:
# change the shader to use a uniform
w.shader_source(vertex_shader,
"""#version 300 es

in vec4 in_position;
uniform vec2 u_pos;

void main() {
  gl_Position = in_position + vec4(u_pos, 0, 0);
}
""")
w.compile_shader(vertex_shader)

# re link the program to also take the uniform
w.link_program(program)

# execute the commands
w.execute_commands(execute_once=True)

# display the program
program
[10]:

Render

Change the commands buffer to render the program with the uniform now.

[11]:
w.clear()
w.use_program(program)
w.uniform('u_pos', np.asarray([0.5,0.5], dtype=np.float32))
w.bind_vertex_array(vao)
w.draw_arrays('TRIANGLES', 0, 3)
w.execute_commands()

Interactive

Let’s tweak the triangle position directly in the notebook using the interact function.

[14]:
from ipywidgets import widgets, interact

def move_triangle(x, y):
    w.clear()
    w.use_program(program)
    w.uniform('u_pos', np.asarray([x, y], dtype=np.float32))
    w.bind_vertex_array(vao)
    w.draw_arrays('TRIANGLES', 0, 3)
    w.execute_commands()

interact(
    move_triangle,
    x=widgets.FloatSlider(min=-1, max=1, step=.01, value=0),
    y=widgets.FloatSlider(min=-1, max=1, step=.01, value=0)
)
w
[14]: