|
| 1 | +import arcade |
| 2 | +from arcade.gl import geometry |
| 3 | +from base_view import BaseView |
| 4 | + |
| 5 | + |
| 6 | +class NormalMapping(BaseView): |
| 7 | + |
| 8 | + def __init__(self, time_on_screen): |
| 9 | + super().__init__(time_on_screen) |
| 10 | + |
| 11 | + # Load the color (diffuse) and normal texture |
| 12 | + # These should ideally be the same size |
| 13 | + self.texture_diffuse = self.window.ctx.load_texture(":resources:images/test_textures/normal_mapping/diffuse.jpg") |
| 14 | + self.texture_normal = self.window.ctx.load_texture(":resources:images/test_textures/normal_mapping/normal.jpg") |
| 15 | + |
| 16 | + # Shader program doing basic normal mapping |
| 17 | + self.program = self.window.ctx.program( |
| 18 | + vertex_shader=""" |
| 19 | + #version 330 |
| 20 | +
|
| 21 | + // Inputs from the quad_fs geometry
8000
|
| 22 | + in vec2 in_vert; |
| 23 | + in vec2 in_uv; |
| 24 | +
|
| 25 | + // Output to the fragment shader |
| 26 | + out vec2 uv; |
| 27 | +
|
| 28 | + void main() { |
| 29 | + uv = in_uv; |
| 30 | + gl_Position = vec4(in_vert, 0.0, 1.0); |
| 31 | + } |
| 32 | +
|
| 33 | + """, |
| 34 | + fragment_shader=""" |
| 35 | + #version 330 |
| 36 | +
|
| 37 | + // Samplers for reading from textures |
| 38 | + uniform sampler2D texture_diffuse; |
| 39 | + uniform sampler2D texture_normal; |
| 40 | + // Global light position we can set from python |
| 41 | + uniform vec3 light_pos; |
| 42 | +
|
| 43 | + // Input from vertex shader |
| 44 | + in vec2 uv; |
| 45 | +
|
| 46 | + // Output to the framebuffer |
| 47 | + out vec4 f_color; |
| 48 | +
|
| 49 | + void main() { |
| 50 | + // Read RGBA color from the diffuse texture |
| 51 | + vec4 diffuse = texture(texture_diffuse, uv); |
| 52 | + // Read normal from RGB channels and convert to a direction vector. |
| 53 | + // These vectors are like a needle per pixel pointing up from the surface. |
| 54 | + // Since RGB is 0-1 we need to convert to -1 to 1. |
| 55 | + vec3 normal = normalize(texture(texture_normal, uv).rgb * 2.0 - 1.0); |
| 56 | +
|
| 57 | + // Calculate the light direction. |
| 58 | + // This is the direction between the light position and the pixel position. |
| 59 | + vec3 light_dir = normalize(light_pos - vec3(uv, 0.0)); |
| 60 | +
|
| 61 | + // Calculate the diffuse factor. |
| 62 | + // This is the dot product between the light direction and the normal. |
| 63 | + // It's basically calculating the angle between the two vectors. |
| 64 | + // The result is a value between 0 and 1. |
| 65 | + float diffuse_factor = max(dot(normal, light_dir), 0.0); |
| 66 | +
|
| 67 | + // Write the final color to the framebuffer. |
| 68 | + // We multiply the diffuse color with the diffuse factor. |
| 69 | + f_color = vec4(diffuse.rgb * diffuse_factor, 1.0); |
| 70 | + } |
| 71 | + """, |
| 72 | + ) |
| 73 | + # Configure what texture channel the samplers should read from |
| 74 | + self.program["texture_diffuse"] = 0 |
| 75 | + self.program["texture_normal"] = 1 |
| 76 | + |
| 77 | + # Shortcut for a full screen quad |
| 78 | + # It has two buffers with positions and texture coordinates |
| 79 | + # named "in_vert" and "in_uv" so we need to use that in the vertex shader |
| 80 | + self.quad_fs = geometry.quad_2d_fs() |
| 81 | + |
| 82 | + # Keep track of mouse coordinates for light position |
| 83 | + self.mouse_x = 0.0 |
| 84 | + self.mouse_y = 0.0 |
| 85 | + self.mouse_z = 0.15 |
| 86 | + |
| 87 | + self.text = arcade.Text("0, 0, 0", 20, 20, arcade.color.WHITE) |
| 88 | + |
| 89 | + def on_draw(self): |
| 90 | + self.clear() |
| 91 | + |
| 92 | + # Bind the textures to the channels we configured in the shader |
| 93 | + self.texture_diffuse.use(0) |
| 94 | + self.texture_normal.use(1) |
| 95 | + |
| 96 | + # Update the light position uniform variable |
| 97 | + self.program["light_pos"] = self.mouse_x, self.mouse_y, self.mouse_z |
| 98 | + |
| 99 | + # Run the normal mapping shader (fills a full screen quad) |
| 100 | + self.quad_fs.render(self.program) |
| 101 | + |
| 102 | + # Draw the mouse coordinates |
| 103 | + self.text.text = f"{self.mouse_x:.2f}, {self.mouse_y:.2f}, {self.mouse_z:.2f}" |
| 104 | + self.text.draw() |
| 105 | + |
| 106 | + self.draw_line_one("Normal Mapping") |
| 107 | + |
| 108 | + def on_update(self, delta_time): |
| 109 | + """ Movement and game logic """ |
| 110 | + |
| 111 | + self.total_time += delta_time |
| 112 | + if self.total_time > self.time_on_screen: |
| 113 | + |
| 114 | + if not self.window.view_list: |
| 115 | + self.window.create_views() |
| 116 | + |
| 117 | + new_view = self.window.view_list.pop(0) |
| 118 | + self.window.show_view(new_view) |
| 119 | + |
| 120 | + self.mouse_x += .003 |
| 121 | + self.mouse_y += .003 |
| 122 | + |
| 123 | + def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int): |
| 124 | + """Zoom in/out with the mouse wheel.""" |
| 125 | + self.mouse_z += scroll_y * 0.05 |
0 commit comments