Render Shadow map¶
We will use a framebuffer to render the shadow map and use that shadow map to draw shadow in our scene.
[1]:
import ipywebgl
import numpy as np
Initialize ipywebgl¶
[2]:
w = ipywebgl.GLViewer()
w.clear_color(.8, .8, .8 ,1)
w.clear()
w.enable(depth_test=True)
w.execute_commands(execute_once=True)
Create framebuffer¶
A float texture set as DEPTH_COMPONENT32F (webgl2)
[3]:
depth_texture = w.create_texture()
w.bind_texture('TEXTURE_2D', depth_texture)
w.tex_image_2d('TEXTURE_2D', 0, 'DEPTH_COMPONENT32F', 512, 512, 0, 'DEPTH_COMPONENT', 'FLOAT', None)
w.tex_parameter('TEXTURE_2D', 'TEXTURE_MAG_FILTER', 'NEAREST')
w.tex_parameter('TEXTURE_2D', 'TEXTURE_MIN_FILTER', 'NEAREST')
w.tex_parameter('TEXTURE_2D', 'TEXTURE_WRAP_S', 'CLAMP_TO_EDGE')
w.tex_parameter('TEXTURE_2D', 'TEXTURE_WRAP_T', 'CLAMP_TO_EDGE')
depth_fb = w.create_framebuffer()
w.bind_framebuffer('FRAMEBUFFER', depth_fb)
w.framebuffer_texture_2d('FRAMEBUFFER', 'DEPTH_ATTACHMENT', 'TEXTURE_2D', depth_texture, 0)
w.bind_framebuffer('FRAMEBUFFER', None)
w.execute_commands(execute_once=True)
Programs¶
Create the two programs. * one that render the scenes and uses the shadowmap * one that just render the scene from the light point of view.
We force the attribute location for the in_vert and in_normal, so we can use both shaders with the same vao’s
[4]:
scene_prog = w.create_program_ext(
"""#version 300 es
//the ViewBlock that is automatically filled by ipywebgl
layout(std140) uniform ViewBlock
{
mat4 u_cameraMatrix; //the camera matrix in world space
mat4 u_viewMatrix; //the inverse of the camera matrix
mat4 u_projectionMatrix; //the projection matrix
mat4 u_viewProjectionMatrix; //the projection * view matrix
};
uniform mat4 u_lightProjection;
uniform mat4 u_world;
uniform vec3 u_lightDir;
in vec3 in_vert;
in vec3 in_normal;
out vec3 v_color;
out vec4 v_shadowcoord;
void main() {
vec4 world = u_world * vec4(in_vert, 1.0);
gl_Position = u_viewProjectionMatrix * world;
v_shadowcoord = u_lightProjection * world;
v_color = vec3(1,1,1) * dot(-u_lightDir, in_normal);
}
"""
,
"""#version 300 es
precision highp float;
uniform float u_bias;
uniform sampler2D u_shadowmap;
in vec3 v_color;
in vec4 v_shadowcoord;
out vec4 f_color;
void main() {
vec3 shadow = v_shadowcoord.xyz / v_shadowcoord.w;
float currentDepth = shadow.z + u_bias;
bool inRange =
shadow.x >= 0.0 &&
shadow.x <= 1.0 &&
shadow.y >= 0.0 &&
shadow.y <= 1.0;
float projectedDepth = texture(u_shadowmap, shadow.xy).r;
float shadowLight = (inRange && projectedDepth < currentDepth) ? 0.0 : 1.0;
f_color = vec4(v_color * shadowLight, 1);
}
""",
{
'in_vert' : 0,
'in_normal' : 1
})
shadow_prog = w.create_program_ext(
"""#version 300 es
uniform mat4 u_lightProjection;
uniform mat4 u_world;
in vec3 in_vert;
void main() {
gl_Position = u_lightProjection * u_world * vec4(in_vert, 1.0);
}
"""
,
"""#version 300 es
precision highp float;
out vec4 f_color;
void main() {
f_color = vec4(1, 0.1, 0.1, 1.0);
}
""",
{'in_vert' : 0})
We will also create a program to render the frustum in the scene. So we can see what is the projection matrix for the light.
[5]:
frustum_prog = w.create_program_ext(
"""#version 300 es
//the ViewBlock that is automatically filled by ipywebgl
layout(std140) uniform ViewBlock
{
mat4 u_cameraMatrix; //the camera matrix in world space
mat4 u_viewMatrix; //the inverse of the camera matrix
mat4 u_projectionMatrix; //the projection matrix
mat4 u_viewProjectionMatrix; //the projection * view matrix
};
uniform mat4 u_lightProjection;
in vec3 in_vert;
void main() {
vec4 world = inverse(u_lightProjection) * vec4(in_vert, 1.0);
world = world/world.w;
gl_Position = u_viewProjectionMatrix * world;
}
"""
,
"""#version 300 es
precision highp float;
out vec4 f_color;
void main() {
f_color = vec4(0,0,0, 1);
}
"""
)
Create the VAO¶
we create vao’s for some isoshpere and a ground plane.
[6]:
sphere_vbo = w.create_buffer_ext(
src_data=np.array(
[[ 0. , 0. , 1. , 0. , 0. , 1. ],
[-0.72, -0.53, 0.45, -0.72, -0.53, 0.45],
[ 0.28, -0.85, 0.45, 0.28, -0.85, 0.45],
[ 0.89, 0. , 0.45, 0.89, 0. , 0.45],
[ 0.28, 0.85, 0.45, 0.28, 0.85, 0.45],
[-0.72, 0.53, 0.45, -0.72, 0.53, 0.45],
[-0.89, -0. , -0.45, -0.89, -0. , -0.45],
[-0.28, -0.85, -0.45, -0.28, -0.85, -0.45],
[ 0.72, -0.53, -0.45, 0.72, -0.53, -0.45],
[ 0.72, 0.53, -0.45, 0.72, 0.53, -0.45],
[-0.28, 0.85, -0.45, -0.28, 0.85, -0.45],
[-0. , 0. , -1. , -0. , 0. , -1. ],
[ 0.16, -0.5 , 0.85, 0.16, -0.5 , 0.85],
[-0.43, -0.31, 0.85, -0.43, -0.31, 0.85],
[-0.26, -0.81, 0.53, -0.26, -0.81, 0.53],
[ 0.53, -0. , 0.85, 0.53, -0. , 0.85],
[ 0.69, -0.5 , 0.53, 0.69, -0.5 , 0.53],
[ 0.16, 0.5 , 0.85, 0.16, 0.5 , 0.85],
[ 0.69, 0.5 , 0.53, 0.69, 0.5 , 0.53],
[-0.43, 0.31, 0.85, -0.43, 0.31, 0.85],
[-0.26, 0.81, 0.53, -0.26, 0.81, 0.53],
[-0.85, -0. , 0.53, -0.85, -0. , 0.53],
[-0.59, -0.81, -0. , -0.59, -0.81, -0. ],
[-0. , -1. , -0. , -0. , -1. , -0. ],
[ 0.59, -0.81, 0. , 0.59, -0.81, 0. ],
[ 0.95, -0.31, -0. , 0.95, -0.31, -0. ],
[ 0.95, 0.31, -0. , 0.95, 0.31, -0. ],
[ 0.59, 0.81, -0. , 0.59, 0.81, -0. ],
[-0. , 1. , -0. , -0. , 1. , -0. ],
[-0.59, 0.81, -0. , -0.59, 0.81, -0. ],
[-0.95, 0.31, 0. , -0.95, 0.31, 0. ],
[-0.95, -0.31, 0. , -0.95, -0.31, 0. ],
[-0.69, -0.5 , -0.53, -0.69, -0.5 , -0.53],
[ 0.26, -0.81, -0.53, 0.26, -0.81, -0.53],
[ 0.85, 0. , -0.53, 0.85, 0. , -0.53],
[ 0.26, 0.81, -0.53, 0.26, 0.81, -0.53],
[-0.69, 0.5 , -0.53, -0.69, 0.5 , -0.53],
[-0.53, -0. , -0.85, -0.53, -0. , -0.85],
[-0.16, -0.5 , -0.85, -0.16, -0.5 , -0.85],
[ 0.43, -0.31, -0.85, 0.43, -0.31, -0.85],
[ 0.43, 0.31, -0.85, 0.43, 0.31, -0.85],
[-0.16, 0.5 , -0.85, -0.16, 0.5 , -0.85]], dtype=np.float32).flatten()
)
indices = np.array(
[[0, 13, 12], [12, 14, 2], [12, 13, 14], [13, 1, 14], [0, 12, 15], [15, 16, 3], [15, 12, 16], [12, 2, 16],
[0, 15, 17], [17, 18, 4], [17, 15, 18], [15, 3, 18], [0, 17, 19], [19, 20, 5], [19, 17, 20], [17, 4, 20],
[0, 19, 13], [13, 21, 1], [13, 19, 21], [19, 5, 21], [1, 22, 14], [14, 23, 2], [14, 22, 23], [22, 7, 23],
[2, 24, 16], [16, 25, 3], [16, 24, 25], [24, 8, 25], [3, 26, 18], [18, 27, 4], [18, 26, 27], [26, 9, 27],
[4, 28, 20], [20, 29, 5], [20, 28, 29], [28, 10, 29], [5, 30, 21], [21, 31, 1], [21, 30, 31], [30, 6, 31],
[1, 31, 22], [22, 32, 7], [22, 31, 32], [31, 6, 32], [2, 23, 24], [24, 33, 8], [24, 23, 33], [23, 7, 33],
[3, 25, 26], [26, 34, 9], [26, 25, 34], [25, 8, 34], [4, 27, 28], [28, 35, 10], [28, 27, 35], [27, 9, 35],
[5, 29, 30], [30, 36, 6], [30, 29, 36], [29, 10, 36], [6, 37, 32], [32, 38, 7], [32, 37, 38], [37, 11, 38],
[7, 38, 33], [33, 39, 8], [33, 38, 39], [38, 11, 39], [8, 39, 34], [34, 40, 9], [34, 39, 40], [39, 11, 40],
[9, 40, 35], [35, 41, 10], [35, 40, 41], [40, 11, 41], [10, 41, 36], [36, 37, 6], [36, 41, 37], [41, 11, 37]],
dtype=np.uint8).flatten()
# because we forced the index of the attribute we can pass in the number directly (instead of the shader and the name)
sphere_vao = w.create_vertex_array_ext(
None,
[
(sphere_vbo, '3f32 3f32', 0, 1),
],
indices
)
plane_vbo = w.create_buffer_ext(
src_data=np.array(
[[10 , 0. , -10. , 0. , 1 , 0. ],
[-10 , 0. , -10. , 0. , 1 , 0. ],
[-10 , 0. , 10. , 0. , 1 , 0. ],
[-10 , 0. , 10. , 0. , 1 , 0. ],
[10 , 0. , 10. , 0. , 1 , 0. ],
[10 , 0. , -10. , 0. , 1 , 0. ]], dtype=np.float32).flatten()
)
plane_vao = w.create_vertex_array_ext(
None,
[
(plane_vbo, '3f32 3f32', 0, 1),
]
)
And we also create a vao for the frustum display. This is just a cube from -1 to 1 in clip space ( the shader will convert it back into world space )
[7]:
frustum_vbo = w.create_buffer_ext(
src_data=np.array(
[-1, -1, -1,
1, -1, -1,
-1, 1, -1,
1, 1, -1,
-1, -1, 1,
1, -1, 1,
-1, 1, 1,
1, 1, 1,], dtype=np.float32)
)
frustum_indices = np.array(
[
0, 1,
1, 3,
3, 2,
2, 0,
4, 5,
5, 7,
7, 6,
6, 4,
0, 4,
1, 5,
3, 7,
2, 6,
], dtype=np.uint8).flatten()
frustum_vao = w.create_vertex_array_ext(
frustum_prog,
[
(frustum_vbo, '3f32', 'in_vert'),
],
frustum_indices
)
Matrices¶
helper functions to create an orthographic and perspective projection matrix. (some function used in javascript to create the ViewProjection uniform )
[8]:
def ortho(width, height, near, far):
A = 1. / width
B = 1. / height
C = -(far + near) / (far - near)
D = -2. / (far - near)
return np.array([
[A, 0, 0, 0],
[0, B, 0, 0],
[0, 0, D, C],
[0, 0, 0, 1]
], dtype=np.float32)
def projection(fov_y, aspect_ratio, near, far):
ymax = near * np.tan(fov_y * np.pi / 360.0)
xmax = ymax * aspect_ratio
def frustum(left, right, bottom, top, near, far):
A = (right + left) / (right - left)
B = (top + bottom) / (top - bottom)
C = -(far + near) / (far - near)
D = -2. * far * near / (far - near)
E = 2. * near / (right - left)
F = 2. * near / (top - bottom)
return np.array(
[
[E, 0, A, 0],
[0, F, B, 0],
[0, 0, C, D],
[0, 0, -1, 0]
], dtype=np.float32)
return frustum(-xmax, xmax, -ymax, ymax, near, far)
Scene¶
Create the scene. * first we create the light matrix * then we multiply it with the projection matrices * this is send to the shader to render the shadow map * then we multiply the projection matrices by the bias matrix to remap the [0,1] space into a [-1,1] space * and we send those matrices to the shader to render the scene with shadows.
[9]:
# light matrix on top looking down
light_matrix = np.eye(4, dtype=np.float32)
light_matrix[:3, 3] = np.array([0,20,20])
light_matrix[:3, 1] = np.array([0,0.707,-0.707])
light_matrix[:3, 2] = np.array([0,0.707,0.707])
inverse_light_dir = light_matrix[:3, 2] * -1
light_matrix = np.linalg.inv(light_matrix)
bias = np.array(
[[0.5, 0.0, 0.0, 0.5],
[0.0, 0.5, 0.0, 0.5],
[0.0, 0.0, 0.5, 0.5],
[0.0, 0.0, 0.0, 1.0]], dtype=np.float32)
# orthographic shadow
light_ortho = ortho(8,8, 10.0, 40.0)
light_ortho_projection = np.dot(light_ortho, light_matrix)
light_ortho_reprojection = np.dot(bias, light_ortho_projection)
# perspective shadow
light_persp = projection(40.0, 1.0, 10.0, 40.0)
light_persp_projection = np.dot(light_persp, light_matrix)
light_persp_reprojection = np.dot(bias, light_persp_projection)
# scene to render
spheres_count = 20
spheres = np.eye(4)[np.newaxis,...].repeat(spheres_count, axis=0)
spheres[:,:3,3] = np.random.random([spheres_count,3]) * 6 - 2.5
spheres[:,1,3] += 3
plane = np.eye(4, dtype=np.float32)
# draw the scene
def _draw_scene():
w.bind_vertex_array(sphere_vao)
for i in range(spheres.shape[0]):
w.uniform_matrix('u_world', spheres[i,:,:].T)
w.draw_elements('TRIANGLES', indices.shape[0], 'UNSIGNED_BYTE', 0)
w.bind_vertex_array(plane_vao)
w.uniform_matrix('u_world', plane.T)
w.draw_arrays('TRIANGLES', 0, 6)
def _render_function(persp=False):
w.enable(depth_test=True)
# draw the shadow map
w.bind_framebuffer('FRAMEBUFFER', depth_fb)
w.viewport(0,0,512,512)
w.clear()
w.use_program(shadow_prog)
if persp:
w.uniform_matrix('u_lightProjection', light_persp_projection.T)
else:
w.uniform_matrix('u_lightProjection', light_ortho_projection.T)
_draw_scene()
# draw the final render
w.bind_framebuffer('FRAMEBUFFER', None)
w.viewport(0,0,w.width,w.height)
w.clear()
w.use_program(scene_prog)
w.active_texture(0)
w.bind_texture('TEXTURE_2D', depth_texture)
if persp:
w.uniform_matrix('u_lightProjection', light_persp_reprojection.T)
else:
w.uniform_matrix('u_lightProjection', light_ortho_reprojection.T)
w.uniform('u_lightDir', inverse_light_dir)
w.uniform('u_bias', np.array([-0.02], dtype=np.float32))
_draw_scene()
# draw the frustum
w.disable(depth_test=True)
w.use_program(frustum_prog)
if persp:
w.uniform_matrix('u_lightProjection', light_persp_projection.T)
else:
w.uniform_matrix('u_lightProjection', light_ortho_projection.T)
w.bind_vertex_array(frustum_vao)
w.draw_elements('LINES', frustum_indices.shape[0], 'UNSIGNED_BYTE', 0)
# render in loop if needed
w.execute_commands()
Display¶
display with the choice of shadow map projection
[11]:
from ipywidgets import widgets, interact
interact(
_render_function,
persp=widgets.Checkbox(description='perspective projection', value=False),
)
w.camera_pos = [33.45616955516928, 37.90264690920414, 36.766131171063385]
w.camera_pitch = -37
w.camera_yaw = 40
w
[11]: