add pixel shader deblocking sample
48
shader_deblocking/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
Python+GLSL Shader Deblocking Sample
|
||||
|
||||
This sample demonstrates how to use a simpler pixel shader to greatly reduce
|
||||
ASTC block artifacts, which can be quite noticeable when the block size goes
|
||||
beyond roughly 6x6. The shader determines if it's going to sample near an edge,
|
||||
and if so it samples a small vertical and horizontal region around the texel
|
||||
and applies a small low pass filter. The example shader is compatible with
|
||||
mipmapping, bilinear filtering, trilinear filtering etc.
|
||||
|
||||
You'll need these Python dependencies to run it:
|
||||
>pip install numpy Pillow glfw PyOpenGL
|
||||
|
||||
See `run.bat` for the command line on how to run the sample. Or run:
|
||||
|
||||
>py -3.12 testbed.py shader.glsl 12 12 flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_0_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_1_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_2_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_3_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_4_face_0_layer_0000.png
|
||||
|
||||
The shader can be easily simplified to sample the texture less by using less taps. The current shader uses a total of 9 taps, but 5 are possible.
|
||||
|
||||
Many variations and optimizations of this basic idea are possible.
|
||||
|
||||
Usage:
|
||||
python testbed.py shader.glsl block_w block_h mip0.png mip1.png [mip2.png ...]
|
||||
block_w, block_h: Block size in texels (e.g. 8 8 for 8x8 DCT blocks)
|
||||
|
||||
Controls:
|
||||
Arrows Move quad left/right/up/down
|
||||
W / S Move closer / farther
|
||||
A / D Rotate yaw (cube mode)
|
||||
Q / E Rotate pitch (cube mode)
|
||||
C Toggle cube / quad mode
|
||||
B Bilinear filtering
|
||||
T Trilinear filtering
|
||||
P Point filtering
|
||||
R Reload shader
|
||||
1 Toggle deblocking shader off/on
|
||||
2 Toggle edge visualization (only when deblocking active)
|
||||
3-4 Toggle shader const0.x/y/z/w (0 <-> 1)
|
||||
5-8 Toggle shader const1.x/y/z/w (0 <-> 1)
|
||||
Space Reset to initial state
|
||||
Esc Quit
|
||||
|
||||
---
|
||||
|
||||
The included sunflower image is in the CC0/Public Domain, and was downloaded from here:
|
||||
|
||||
https://www.publicdomainpictures.net/en/view-image.php?image=756601&picture=large-yellow-sunflower
|
||||
"License: CC0 Public Domain - Lynn Greyling has released this “Large Yellow Sunflower” image under Public Domain license."
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 72 B |
|
After Width: | Height: | Size: 386 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 526 B |
|
After Width: | Height: | Size: 175 B |
|
After Width: | Height: | Size: 94 B |
|
After Width: | Height: | Size: 75 B |
1
shader_deblocking/run.bat
Normal file
@@ -0,0 +1 @@
|
||||
py -3.12 testbed.py shader.glsl 12 12 flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_0_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_1_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_2_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_3_face_0_layer_0000.png flower_unpacked_rgb_ASTC_LDR_12X12_RGBA_level_4_face_0_layer_0000.png
|
||||
97
shader_deblocking/shader.glsl
Normal file
@@ -0,0 +1,97 @@
|
||||
#vertex
|
||||
#version 330 core
|
||||
|
||||
layout(location = 0) in vec3 aPos;
|
||||
layout(location = 1) in vec2 aUV;
|
||||
|
||||
uniform mat4 mvp;
|
||||
|
||||
out vec2 vUV;
|
||||
|
||||
void main() {
|
||||
vUV = aUV;
|
||||
gl_Position = mvp * vec4(aPos, 1.0);
|
||||
}
|
||||
|
||||
#fragment
|
||||
#version 330 core
|
||||
|
||||
uniform sampler2D tex;
|
||||
uniform vec4 texSize; // Base mip dimensions (mip 0)
|
||||
uniform vec4 const0; // User constant 0 (keys 1-4 toggle x,y,z,w)
|
||||
uniform vec4 const1; // User constant 1 (keys 5-8 toggle x,y,z,w)
|
||||
|
||||
in vec2 vUV;
|
||||
out vec4 fragColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 blockSize = vec2(texSize.z, texSize.w);
|
||||
|
||||
vec2 du = dFdx(vUV);
|
||||
vec2 dv = dFdy(vUV);
|
||||
float rho = max(length(du * texSize.xy), length(dv * texSize.xy));
|
||||
float lod = max(0.0, log2(max(rho, 1e-8))); // lod index
|
||||
float mipScale = exp2(floor(lod + .5)); // 2^lod mipmap scale, snaps to dominant mipmap
|
||||
|
||||
vec2 texDim = vec2(texSize.x, texSize.y);
|
||||
vec2 texelStep = 1.0 / texDim;
|
||||
vec2 texelPos = (vUV * texDim) / mipScale;
|
||||
vec2 blockPos = mod(texelPos, blockSize);
|
||||
|
||||
vec3 color;
|
||||
color = texture(tex, vUV).rgb;
|
||||
|
||||
if (const0.x > 0.5)
|
||||
{
|
||||
float falloff = 2.0;
|
||||
|
||||
float leftProx = 1.0 - clamp(blockPos.x / falloff, 0.0, 1.0);
|
||||
float rightProx = 1.0 - clamp((blockSize.x - 1.0 - blockPos.x) / falloff, 0.0, 1.0);
|
||||
float topProx = 1.0 - clamp(blockPos.y / falloff, 0.0, 1.0);
|
||||
float bottomProx = 1.0 - clamp((blockSize.y - 1.0 - blockPos.y) / falloff, 0.0, 1.0);
|
||||
|
||||
float horizWeight = max(leftProx, rightProx);
|
||||
float vertWeight = max(topProx, bottomProx);
|
||||
float edgeWeight = max(horizWeight, vertWeight); // overall proximity
|
||||
|
||||
vec3 c0 = color; //texture2D(tex, vUV).rgb;
|
||||
|
||||
vec3 l2 = texture2D(tex, vUV - vec2(2 * texelStep.x * mipScale, 0.0)).rgb;
|
||||
vec3 l1 = texture2D(tex, vUV - vec2(texelStep.x * mipScale, 0.0)).rgb;
|
||||
vec3 r1 = texture2D(tex, vUV + vec2(texelStep.x * mipScale, 0.0)).rgb;
|
||||
vec3 r2 = texture2D(tex, vUV + vec2(2 * texelStep.x * mipScale, 0.0)).rgb;
|
||||
|
||||
vec3 u2 = texture2D(tex, vUV - vec2(0.0, 2 * texelStep.y * mipScale)).rgb;
|
||||
vec3 u1 = texture2D(tex, vUV - vec2(0.0, texelStep.y * mipScale)).rgb;
|
||||
vec3 d1 = texture2D(tex, vUV + vec2(0.0, texelStep.y * mipScale)).rgb;
|
||||
vec3 d2 = texture2D(tex, vUV + vec2(0.0, 2 * texelStep.y * mipScale)).rgb;
|
||||
|
||||
//vec3 filteredH = (l2 + 2 * l1 + 3 * c0 + 2 * r1 + r2) / 9.0;
|
||||
//vec3 filteredV = (u2 + 2 * u1 + 3 * c0 + 2 * d1 + d2) / 9.0;
|
||||
|
||||
vec3 filteredH = (l2 + 2 * l1 + 2 * c0 + 2 * r1 + r2) / 8.0;
|
||||
vec3 filteredV = (u2 + 2 * u1 + 2 * c0 + 2 * d1 + d2) / 8.0;
|
||||
|
||||
float smoothH = 1.0;
|
||||
float smoothV = 1.0;
|
||||
|
||||
if (edgeWeight > 0.0)
|
||||
{
|
||||
vec3 horizColor = mix(c0, filteredH, smoothH * horizWeight);
|
||||
vec3 vertColor = mix(c0, filteredV, smoothV * vertWeight);
|
||||
|
||||
float totalW = horizWeight + vertWeight;
|
||||
if (totalW > 0.0)
|
||||
color = (horizColor * horizWeight + vertColor * vertWeight) / totalW;
|
||||
}
|
||||
|
||||
// block edge vis
|
||||
if (const0.y > 0.5)
|
||||
{
|
||||
color = vec3(edgeWeight, edgeWeight, edgeWeight);
|
||||
}
|
||||
}
|
||||
|
||||
fragColor = vec4(color, 1.0);
|
||||
}
|
||||
861
shader_deblocking/testbed.py
Normal file
@@ -0,0 +1,861 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Mipmap Deblocking Shader Testbed
|
||||
Copyright (C) 2026 Binomial LLC.
|
||||
LICENSE: Apache 2.0
|
||||
|
||||
Usage:
|
||||
python testbed.py shader.glsl block_w block_h mip0.png mip1.png [mip2.png ...]
|
||||
block_w, block_h: Block size in texels (e.g. 8 8 for 8x8 DCT blocks)
|
||||
|
||||
Controls:
|
||||
Arrows Move quad left/right/up/down
|
||||
W / S Move closer / farther
|
||||
A / D Rotate yaw (cube mode)
|
||||
Q / E Rotate pitch (cube mode)
|
||||
C Toggle cube / quad mode
|
||||
B Bilinear filtering
|
||||
T Trilinear filtering
|
||||
P Point filtering
|
||||
R Reload shader
|
||||
1 Toggle deblocking shader off/on
|
||||
2 Toggle edge visualization (only when deblocking active)
|
||||
3-4 Toggle shader const0.x/y/z/w (0 <-> 1)
|
||||
5-8 Toggle shader const1.x/y/z/w (0 <-> 1)
|
||||
Space Reset to initial state
|
||||
Esc Quit
|
||||
"""
|
||||
|
||||
import sys, os, importlib.util
|
||||
print("=== DIAG ===")
|
||||
print("exe:", sys.executable)
|
||||
print("ver:", sys.version)
|
||||
print("cwd:", os.getcwd())
|
||||
print("glfw spec:", importlib.util.find_spec("glfw"))
|
||||
print("OpenGL spec:", importlib.util.find_spec("OpenGL"))
|
||||
print("============")
|
||||
|
||||
import sys
|
||||
import ctypes
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from pathlib import Path
|
||||
|
||||
import glfw
|
||||
from OpenGL.GL import *
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Globals
|
||||
# -----------------------------------------------------------------------------
|
||||
WINDOW_WIDTH = 1280
|
||||
WINDOW_HEIGHT = 720
|
||||
FOV_DEGREES = 90.0
|
||||
Z_MIN = .40
|
||||
Z_MAX = -50.0
|
||||
Z_SPEED = 2.0
|
||||
XY_SPEED = 1.5
|
||||
ROT_SPEED = 90.0 # degrees per second
|
||||
# Block size (set from command line)
|
||||
BLOCK_WIDTH = 12
|
||||
BLOCK_HEIGHT = 12
|
||||
|
||||
g_state = {
|
||||
'x': 0.0,
|
||||
'y': 0.0,
|
||||
'z': -3.0,
|
||||
'yaw': 0.0,
|
||||
'pitch': 0.0,
|
||||
'mode': 'QUAD', # 'QUAD' or 'CUBE'
|
||||
'filter_mode': 'BILINEAR',
|
||||
'shader_path': None,
|
||||
'program': None,
|
||||
'texture': None,
|
||||
'tex_size': (0, 0),
|
||||
'quad_vao': None,
|
||||
'cube_vao': None,
|
||||
'cube_index_count': 0,
|
||||
'debug_vao': None,
|
||||
'debug_texture': None,
|
||||
'debug_dirty': True,
|
||||
'last_time': 0.0,
|
||||
'const0': [0.0, 0.0, 0.0, 0.0],
|
||||
'const1': [0.0, 0.0, 0.0, 0.0],
|
||||
}
|
||||
INIT_X = 0.0
|
||||
INIT_Y = 0.0
|
||||
INIT_Z = -3.0
|
||||
INIT_YAW = 0.0
|
||||
INIT_PITCH = 0.0
|
||||
INIT_CONST0 = [0.0, 0.0, 0.0, 0.0]
|
||||
INIT_CONST1 = [0.0, 0.0, 0.0, 0.0]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Shader Loading
|
||||
# -----------------------------------------------------------------------------
|
||||
def parse_shader_file(path):
|
||||
"""Parse shader file with #vertex and #fragment markers."""
|
||||
text = Path(path).read_text()
|
||||
|
||||
vertex_src = None
|
||||
fragment_src = None
|
||||
|
||||
parts = text.split('#vertex')
|
||||
if len(parts) < 2:
|
||||
print(f"ERROR: No #vertex marker found in {path}")
|
||||
return None, None
|
||||
|
||||
rest = parts[1]
|
||||
frag_parts = rest.split('#fragment')
|
||||
if len(frag_parts) < 2:
|
||||
print(f"ERROR: No #fragment marker found in {path}")
|
||||
return None, None
|
||||
|
||||
vertex_src = frag_parts[0].strip()
|
||||
fragment_src = frag_parts[1].strip()
|
||||
|
||||
return vertex_src, fragment_src
|
||||
|
||||
|
||||
def compile_shader(source, shader_type):
|
||||
"""Compile a shader, return handle or None on error."""
|
||||
shader = glCreateShader(shader_type)
|
||||
glShaderSource(shader, source)
|
||||
glCompileShader(shader)
|
||||
|
||||
if glGetShaderiv(shader, GL_COMPILE_STATUS) != GL_TRUE:
|
||||
error = glGetShaderInfoLog(shader)
|
||||
if isinstance(error, bytes):
|
||||
error = error.decode('utf-8')
|
||||
type_name = "VERTEX" if shader_type == GL_VERTEX_SHADER else "FRAGMENT"
|
||||
print(f"{type_name} SHADER ERROR:\n{error}")
|
||||
glDeleteShader(shader)
|
||||
return None
|
||||
|
||||
return shader
|
||||
|
||||
|
||||
def link_program(vertex_shader, fragment_shader):
|
||||
"""Link shaders into program, return handle or None on error."""
|
||||
program = glCreateProgram()
|
||||
glAttachShader(program, vertex_shader)
|
||||
glAttachShader(program, fragment_shader)
|
||||
glLinkProgram(program)
|
||||
|
||||
if glGetProgramiv(program, GL_LINK_STATUS) != GL_TRUE:
|
||||
error = glGetProgramInfoLog(program)
|
||||
if isinstance(error, bytes):
|
||||
error = error.decode('utf-8')
|
||||
print(f"LINK ERROR:\n{error}")
|
||||
glDeleteProgram(program)
|
||||
return None
|
||||
|
||||
return program
|
||||
|
||||
|
||||
def load_shader(path):
|
||||
"""Load, compile, and link shader from file. Returns program or None."""
|
||||
print(f"Loading shader: {path}")
|
||||
|
||||
vertex_src, fragment_src = parse_shader_file(path)
|
||||
if vertex_src is None or fragment_src is None:
|
||||
return None
|
||||
|
||||
vertex_shader = compile_shader(vertex_src, GL_VERTEX_SHADER)
|
||||
if vertex_shader is None:
|
||||
return None
|
||||
|
||||
fragment_shader = compile_shader(fragment_src, GL_FRAGMENT_SHADER)
|
||||
if fragment_shader is None:
|
||||
glDeleteShader(vertex_shader)
|
||||
return None
|
||||
|
||||
program = link_program(vertex_shader, fragment_shader)
|
||||
|
||||
glDeleteShader(vertex_shader)
|
||||
glDeleteShader(fragment_shader)
|
||||
|
||||
if program:
|
||||
print("Shader compiled successfully.")
|
||||
|
||||
return program
|
||||
|
||||
|
||||
def reload_shader():
|
||||
"""Attempt to reload shader. Keep old one if failed."""
|
||||
new_program = load_shader(g_state['shader_path'])
|
||||
if new_program is not None:
|
||||
if g_state['program'] is not None:
|
||||
glDeleteProgram(g_state['program'])
|
||||
g_state['program'] = new_program
|
||||
else:
|
||||
print("Shader reload failed, keeping previous shader.")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Texture Loading
|
||||
# -----------------------------------------------------------------------------
|
||||
def load_mipmap_texture(paths):
|
||||
"""Load PNG files as mipmap levels. Returns texture handle and base size."""
|
||||
images = []
|
||||
|
||||
for i, path in enumerate(paths):
|
||||
img = Image.open(path).convert('RGBA')
|
||||
images.append(img)
|
||||
print(f"Loaded mip {i}: {path} ({img.width}x{img.height})")
|
||||
|
||||
# Validate dimensions
|
||||
for i in range(1, len(images)):
|
||||
expected_w = images[i - 1].width // 2
|
||||
expected_h = images[i - 1].height // 2
|
||||
actual_w = images[i].width
|
||||
actual_h = images[i].height
|
||||
|
||||
if actual_w != expected_w or actual_h != expected_h:
|
||||
print(f"ERROR: Mip {i} should be {expected_w}x{expected_h}, got {actual_w}x{actual_h}")
|
||||
sys.exit(1)
|
||||
|
||||
# Create texture
|
||||
texture = glGenTextures(1)
|
||||
glBindTexture(GL_TEXTURE_2D, texture)
|
||||
|
||||
# Upload each mip level
|
||||
for level, img in enumerate(images):
|
||||
data = np.array(img, dtype=np.uint8)
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D, level, GL_RGBA8,
|
||||
img.width, img.height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, data
|
||||
)
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, len(images) - 1)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
||||
|
||||
base_size = (images[0].width, images[0].height)
|
||||
return texture, base_size
|
||||
|
||||
|
||||
def set_filter_mode(mode):
|
||||
"""Set texture filtering mode."""
|
||||
glBindTexture(GL_TEXTURE_2D, g_state['texture'])
|
||||
|
||||
if mode == 'BILINEAR':
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
||||
elif mode == 'TRILINEAR':
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
||||
else: # POINT
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
|
||||
|
||||
g_state['filter_mode'] = mode
|
||||
g_state['debug_dirty'] = True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Geometry
|
||||
# -----------------------------------------------------------------------------
|
||||
def create_quad(aspect_ratio):
|
||||
"""Create a quad VAO centered at origin with given aspect ratio."""
|
||||
# Normalize so longest dimension is 1.0
|
||||
if aspect_ratio >= 1.0:
|
||||
half_w = 1.0
|
||||
half_h = 1.0 / aspect_ratio
|
||||
else:
|
||||
half_w = aspect_ratio
|
||||
half_h = 1.0
|
||||
|
||||
# Position (x, y, z) + UV (u, v)
|
||||
vertices = np.array([
|
||||
-half_w, -half_h, 0.0, 0.0, 1.0,
|
||||
half_w, -half_h, 0.0, 1.0, 1.0,
|
||||
half_w, half_h, 0.0, 1.0, 0.0,
|
||||
-half_w, half_h, 0.0, 0.0, 0.0,
|
||||
], dtype=np.float32)
|
||||
|
||||
indices = np.array([0, 1, 2, 0, 2, 3], dtype=np.uint32)
|
||||
|
||||
vao = glGenVertexArrays(1)
|
||||
vbo = glGenBuffers(1)
|
||||
ebo = glGenBuffers(1)
|
||||
|
||||
glBindVertexArray(vao)
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo)
|
||||
glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
|
||||
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo)
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW)
|
||||
|
||||
# Position attribute (location 0)
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 20, ctypes.c_void_p(0))
|
||||
glEnableVertexAttribArray(0)
|
||||
|
||||
# UV attribute (location 1)
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 20, ctypes.c_void_p(12))
|
||||
glEnableVertexAttribArray(1)
|
||||
|
||||
glBindVertexArray(0)
|
||||
|
||||
return vao
|
||||
|
||||
|
||||
def create_cube(size=1.0):
|
||||
"""Create a textured cube VAO centered at origin."""
|
||||
h = size / 2.0
|
||||
|
||||
# Each face: 4 vertices with position (x,y,z) + UV (u,v)
|
||||
# Front face (z = +h)
|
||||
front = [
|
||||
-h, -h, h, 0.0, 1.0,
|
||||
h, -h, h, 1.0, 1.0,
|
||||
h, h, h, 1.0, 0.0,
|
||||
-h, h, h, 0.0, 0.0,
|
||||
]
|
||||
# Back face (z = -h)
|
||||
back = [
|
||||
h, -h, -h, 0.0, 1.0,
|
||||
-h, -h, -h, 1.0, 1.0,
|
||||
-h, h, -h, 1.0, 0.0,
|
||||
h, h, -h, 0.0, 0.0,
|
||||
]
|
||||
# Right face (x = +h)
|
||||
right = [
|
||||
h, -h, h, 0.0, 1.0,
|
||||
h, -h, -h, 1.0, 1.0,
|
||||
h, h, -h, 1.0, 0.0,
|
||||
h, h, h, 0.0, 0.0,
|
||||
]
|
||||
# Left face (x = -h)
|
||||
left = [
|
||||
-h, -h, -h, 0.0, 1.0,
|
||||
-h, -h, h, 1.0, 1.0,
|
||||
-h, h, h, 1.0, 0.0,
|
||||
-h, h, -h, 0.0, 0.0,
|
||||
]
|
||||
# Top face (y = +h)
|
||||
top = [
|
||||
-h, h, h, 0.0, 1.0,
|
||||
h, h, h, 1.0, 1.0,
|
||||
h, h, -h, 1.0, 0.0,
|
||||
-h, h, -h, 0.0, 0.0,
|
||||
]
|
||||
# Bottom face (y = -h)
|
||||
bottom = [
|
||||
-h, -h, -h, 0.0, 1.0,
|
||||
h, -h, -h, 1.0, 1.0,
|
||||
h, -h, h, 1.0, 0.0,
|
||||
-h, -h, h, 0.0, 0.0,
|
||||
]
|
||||
|
||||
vertices = np.array(front + back + right + left + top + bottom, dtype=np.float32)
|
||||
|
||||
# 6 faces, each with 2 triangles (6 indices per face)
|
||||
indices = []
|
||||
for i in range(6):
|
||||
base = i * 4
|
||||
indices.extend([base, base+1, base+2, base, base+2, base+3])
|
||||
indices = np.array(indices, dtype=np.uint32)
|
||||
|
||||
vao = glGenVertexArrays(1)
|
||||
vbo = glGenBuffers(1)
|
||||
ebo = glGenBuffers(1)
|
||||
|
||||
glBindVertexArray(vao)
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo)
|
||||
glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
|
||||
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo)
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW)
|
||||
|
||||
# Position attribute (location 0)
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 20, ctypes.c_void_p(0))
|
||||
glEnableVertexAttribArray(0)
|
||||
|
||||
# UV attribute (location 1)
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 20, ctypes.c_void_p(12))
|
||||
glEnableVertexAttribArray(1)
|
||||
|
||||
glBindVertexArray(0)
|
||||
|
||||
return vao, len(indices)
|
||||
|
||||
|
||||
def create_debug_quad():
|
||||
"""Create a screen-space quad for debug text overlay."""
|
||||
# Screen-space quad at top-left
|
||||
# NDC: x=-1 is left, y=1 is top
|
||||
w = 680.0 / WINDOW_WIDTH * 2.0
|
||||
h = 60.0 / WINDOW_HEIGHT * 2.0
|
||||
|
||||
vertices = np.array([
|
||||
-1.0, 1.0, 0.0, 0.0,
|
||||
-1.0 + w, 1.0, 1.0, 0.0,
|
||||
-1.0 + w, 1.0 - h, 1.0, 1.0,
|
||||
-1.0, 1.0 - h, 0.0, 1.0,
|
||||
], dtype=np.float32)
|
||||
|
||||
indices = np.array([0, 1, 2, 0, 2, 3], dtype=np.uint32)
|
||||
|
||||
vao = glGenVertexArrays(1)
|
||||
vbo = glGenBuffers(1)
|
||||
ebo = glGenBuffers(1)
|
||||
|
||||
glBindVertexArray(vao)
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo)
|
||||
glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
|
||||
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo)
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW)
|
||||
|
||||
# Position attribute (location 0) - xy only
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 16, ctypes.c_void_p(0))
|
||||
glEnableVertexAttribArray(0)
|
||||
|
||||
# UV attribute (location 1)
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 16, ctypes.c_void_p(8))
|
||||
glEnableVertexAttribArray(1)
|
||||
|
||||
glBindVertexArray(0)
|
||||
|
||||
return vao
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Debug Text
|
||||
# -----------------------------------------------------------------------------
|
||||
DEBUG_VERTEX = """
|
||||
#version 330 core
|
||||
layout(location = 0) in vec2 aPos;
|
||||
layout(location = 1) in vec2 aUV;
|
||||
out vec2 vUV;
|
||||
void main() {
|
||||
vUV = aUV;
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
DEBUG_FRAGMENT = """
|
||||
#version 330 core
|
||||
uniform sampler2D tex;
|
||||
in vec2 vUV;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = texture(tex, vUV);
|
||||
}
|
||||
"""
|
||||
|
||||
g_debug_program = None
|
||||
|
||||
|
||||
def init_debug_rendering():
|
||||
"""Initialize debug text rendering resources."""
|
||||
global g_debug_program
|
||||
|
||||
vs = compile_shader(DEBUG_VERTEX, GL_VERTEX_SHADER)
|
||||
fs = compile_shader(DEBUG_FRAGMENT, GL_FRAGMENT_SHADER)
|
||||
|
||||
if vs is None or fs is None:
|
||||
print("ERROR: Failed to compile debug shaders")
|
||||
if vs:
|
||||
glDeleteShader(vs)
|
||||
if fs:
|
||||
glDeleteShader(fs)
|
||||
return
|
||||
|
||||
g_debug_program = link_program(vs, fs)
|
||||
glDeleteShader(vs)
|
||||
glDeleteShader(fs)
|
||||
|
||||
if g_debug_program is None:
|
||||
print("ERROR: Failed to link debug program")
|
||||
return
|
||||
|
||||
g_state['debug_vao'] = create_debug_quad()
|
||||
g_state['debug_texture'] = glGenTextures(1)
|
||||
|
||||
|
||||
def update_debug_text():
|
||||
"""Render debug text to texture."""
|
||||
if not g_state['debug_dirty']:
|
||||
return
|
||||
|
||||
c0 = g_state['const0']
|
||||
c1 = g_state['const1']
|
||||
|
||||
# Build status lines
|
||||
lines = [
|
||||
f"Mode:{g_state['mode']:4s} Filter:{g_state['filter_mode']:9s} Block:{BLOCK_WIDTH}x{BLOCK_HEIGHT} Deblock: [{int(c0[0])}{int(c0[1])}{int(c0[2])}{int(c0[3])}][{int(c1[0])}{int(c1[1])}{int(c1[2])}{int(c1[3])}]",
|
||||
f"X:{g_state['x']:+5.1f} Y:{g_state['y']:+5.1f} Z:{g_state['z']:5.1f} Yaw:{g_state['yaw']:+6.1f} Pitch:{g_state['pitch']:+6.1f}",
|
||||
"Arrows:move, W/S:zoom, A/D:yaw, Q/E:pitch, C:cube, B/T/P:filter, 1=deblocking toggle, 2=edge vis, R:reload, Space:reset",
|
||||
]
|
||||
|
||||
img = Image.new('RGBA', (680, 60), (0, 0, 0, 180))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 14)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
y = 4
|
||||
for line in lines:
|
||||
draw.text((6, y), line, fill=(255, 255, 255, 255), font=font)
|
||||
y += 18
|
||||
|
||||
data = np.array(img, dtype=np.uint8)
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, g_state['debug_texture'])
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.width, img.height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, data)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
||||
|
||||
g_state['debug_dirty'] = False
|
||||
|
||||
|
||||
def draw_debug_text():
|
||||
"""Draw debug text overlay."""
|
||||
if g_debug_program is None:
|
||||
return
|
||||
|
||||
update_debug_text()
|
||||
|
||||
glEnable(GL_BLEND)
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
||||
glDisable(GL_DEPTH_TEST)
|
||||
|
||||
glUseProgram(g_debug_program)
|
||||
glBindTexture(GL_TEXTURE_2D, g_state['debug_texture'])
|
||||
glBindVertexArray(g_state['debug_vao'])
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None)
|
||||
|
||||
glEnable(GL_DEPTH_TEST)
|
||||
glDisable(GL_BLEND)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Math
|
||||
# -----------------------------------------------------------------------------
|
||||
def perspective_matrix(fov_deg, aspect, near, far):
|
||||
"""Create perspective projection matrix."""
|
||||
fov_rad = np.radians(fov_deg)
|
||||
f = 1.0 / np.tan(fov_rad / 2.0)
|
||||
|
||||
m = np.zeros((4, 4), dtype=np.float32)
|
||||
m[0, 0] = f / aspect
|
||||
m[1, 1] = f
|
||||
m[2, 2] = (far + near) / (near - far)
|
||||
m[2, 3] = (2 * far * near) / (near - far)
|
||||
m[3, 2] = -1.0
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def translation_matrix(x, y, z):
|
||||
"""Create translation matrix."""
|
||||
m = np.eye(4, dtype=np.float32)
|
||||
m[0, 3] = x
|
||||
m[1, 3] = y
|
||||
m[2, 3] = z
|
||||
return m
|
||||
|
||||
|
||||
def rotation_matrix_y(deg):
|
||||
"""Create rotation matrix around Y axis (yaw)."""
|
||||
rad = np.radians(deg)
|
||||
c, s = np.cos(rad), np.sin(rad)
|
||||
m = np.eye(4, dtype=np.float32)
|
||||
m[0, 0] = c
|
||||
m[0, 2] = s
|
||||
m[2, 0] = -s
|
||||
m[2, 2] = c
|
||||
return m
|
||||
|
||||
|
||||
def rotation_matrix_x(deg):
|
||||
"""Create rotation matrix around X axis (pitch)."""
|
||||
rad = np.radians(deg)
|
||||
c, s = np.cos(rad), np.sin(rad)
|
||||
m = np.eye(4, dtype=np.float32)
|
||||
m[1, 1] = c
|
||||
m[1, 2] = -s
|
||||
m[2, 1] = s
|
||||
m[2, 2] = c
|
||||
return m
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Input
|
||||
# -----------------------------------------------------------------------------
|
||||
def framebuffer_size_callback(window, width, height):
|
||||
"""Handle window resize."""
|
||||
global WINDOW_WIDTH, WINDOW_HEIGHT
|
||||
WINDOW_WIDTH = width
|
||||
WINDOW_HEIGHT = height
|
||||
glViewport(0, 0, width, height)
|
||||
g_state['debug_dirty'] = True
|
||||
|
||||
|
||||
def key_callback(window, key, scancode, action, mods):
|
||||
if action == glfw.PRESS:
|
||||
if key == glfw.KEY_ESCAPE:
|
||||
glfw.set_window_should_close(window, True)
|
||||
elif key == glfw.KEY_R:
|
||||
reload_shader()
|
||||
elif key == glfw.KEY_B:
|
||||
set_filter_mode('BILINEAR')
|
||||
print("Filter: BILINEAR")
|
||||
elif key == glfw.KEY_P:
|
||||
set_filter_mode('POINT')
|
||||
print("Filter: POINT")
|
||||
elif key == glfw.KEY_T:
|
||||
set_filter_mode('TRILINEAR')
|
||||
print("Filter: TRILINEAR")
|
||||
# Toggle const0 components (keys 1-4)
|
||||
elif key == glfw.KEY_1:
|
||||
g_state['const0'][0] = 1.0 - g_state['const0'][0]
|
||||
print(f"const0: {g_state['const0']}")
|
||||
g_state['debug_dirty'] = True
|
||||
elif key == glfw.KEY_2:
|
||||
g_state['const0'][1] = 1.0 - g_state['const0'][1]
|
||||
print(f"const0: {g_state['const0']}")
|
||||
g_state['debug_dirty'] = True
|
||||
elif key == glfw.KEY_3:
|
||||
g_state['const0'][2] = 1.0 - g_state['const0'][2]
|
||||
print(f"const0: {g_state['const0']}")
|
||||
g_state['debug_dirty'] = True
|
||||
elif key == glfw.KEY_4:
|
||||
g_state['const0'][3] = 1.0 - g_state['const0'][3]
|
||||
print(f"const0: {g_state['const0']}")
|
||||
g_state['debug_dirty'] = True
|
||||
# Toggle const1 components (keys 5-8)
|
||||
elif key == glfw.KEY_5:
|
||||
g_state['const1'][0] = 1.0 - g_state['const1'][0]
|
||||
print(f"const1: {g_state['const1']}")
|
||||
g_state['debug_dirty'] = True
|
||||
elif key == glfw.KEY_6:
|
||||
g_state['const1'][1] = 1.0 - g_state['const1'][1]
|
||||
print(f"const1: {g_state['const1']}")
|
||||
g_state['debug_dirty'] = True
|
||||
elif key == glfw.KEY_7:
|
||||
g_state['const1'][2] = 1.0 - g_state['const1'][2]
|
||||
print(f"const1: {g_state['const1']}")
|
||||
g_state['debug_dirty'] = True
|
||||
elif key == glfw.KEY_8:
|
||||
g_state['const1'][3] = 1.0 - g_state['const1'][3]
|
||||
print(f"const1: {g_state['const1']}")
|
||||
g_state['debug_dirty'] = True
|
||||
elif key == glfw.KEY_C:
|
||||
g_state['mode'] = 'CUBE' if g_state['mode'] == 'QUAD' else 'QUAD'
|
||||
print(f"Mode: {g_state['mode']}")
|
||||
g_state['debug_dirty'] = True
|
||||
elif key == glfw.KEY_SPACE:
|
||||
g_state['x'] = INIT_X
|
||||
g_state['y'] = INIT_Y
|
||||
g_state['z'] = INIT_Z
|
||||
g_state['yaw'] = INIT_YAW
|
||||
g_state['pitch'] = INIT_PITCH
|
||||
g_state['const0'] = INIT_CONST0.copy()
|
||||
g_state['const1'] = INIT_CONST1.copy()
|
||||
g_state['debug_dirty'] = True
|
||||
print("Reset to initial state")
|
||||
|
||||
|
||||
def process_held_keys(window, dt):
|
||||
"""Process continuously held keys."""
|
||||
moved = False
|
||||
|
||||
if glfw.get_key(window, glfw.KEY_W) == glfw.PRESS:
|
||||
g_state['z'] += Z_SPEED * dt
|
||||
moved = True
|
||||
|
||||
if glfw.get_key(window, glfw.KEY_S) == glfw.PRESS:
|
||||
g_state['z'] -= Z_SPEED * dt
|
||||
moved = True
|
||||
if glfw.get_key(window, glfw.KEY_LEFT) == glfw.PRESS:
|
||||
g_state['x'] += XY_SPEED * dt
|
||||
moved = True
|
||||
if glfw.get_key(window, glfw.KEY_RIGHT) == glfw.PRESS:
|
||||
g_state['x'] -= XY_SPEED * dt
|
||||
moved = True
|
||||
if glfw.get_key(window, glfw.KEY_UP) == glfw.PRESS:
|
||||
g_state['y'] += XY_SPEED * dt
|
||||
moved = True
|
||||
if glfw.get_key(window, glfw.KEY_DOWN) == glfw.PRESS:
|
||||
g_state['y'] -= XY_SPEED * dt
|
||||
moved = True
|
||||
|
||||
# Rotation (A/D for yaw, Q/E for pitch)
|
||||
if glfw.get_key(window, glfw.KEY_A) == glfw.PRESS:
|
||||
g_state['yaw'] += ROT_SPEED * dt
|
||||
moved = True
|
||||
if glfw.get_key(window, glfw.KEY_D) == glfw.PRESS:
|
||||
g_state['yaw'] -= ROT_SPEED * dt
|
||||
moved = True
|
||||
if glfw.get_key(window, glfw.KEY_Q) == glfw.PRESS:
|
||||
g_state['pitch'] += ROT_SPEED * dt
|
||||
moved = True
|
||||
if glfw.get_key(window, glfw.KEY_E) == glfw.PRESS:
|
||||
g_state['pitch'] -= ROT_SPEED * dt
|
||||
moved = True
|
||||
|
||||
# Clamp Z
|
||||
g_state['z'] = max(Z_MAX, min(Z_MIN, g_state['z']))
|
||||
|
||||
if moved:
|
||||
g_state['debug_dirty'] = True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
global BLOCK_WIDTH, BLOCK_HEIGHT
|
||||
if len(sys.argv) < 5:
|
||||
print(__doc__)
|
||||
print("ERROR: Need shader, block_w, block_h, and at least one mipmap PNG")
|
||||
print("Example: python testbed.py shader.glsl 8 8 mip0.png mip1.png")
|
||||
sys.exit(1)
|
||||
|
||||
shader_path = sys.argv[1]
|
||||
try:
|
||||
BLOCK_WIDTH = int(sys.argv[2])
|
||||
BLOCK_HEIGHT = int(sys.argv[3])
|
||||
except ValueError:
|
||||
print(f"ERROR: block_w and block_h must be integers, got '{sys.argv[2]}' '{sys.argv[3]}'")
|
||||
sys.exit(1)
|
||||
if BLOCK_WIDTH < 1 or BLOCK_HEIGHT < 1:
|
||||
print(f"ERROR: block size must be positive, got {BLOCK_WIDTH}x{BLOCK_HEIGHT}")
|
||||
sys.exit(1)
|
||||
mip_paths = sys.argv[4:]
|
||||
print(f"Block size: {BLOCK_WIDTH}x{BLOCK_HEIGHT}")
|
||||
|
||||
g_state['shader_path'] = shader_path
|
||||
|
||||
# Init GLFW
|
||||
if not glfw.init():
|
||||
print("ERROR: Failed to initialize GLFW")
|
||||
sys.exit(1)
|
||||
|
||||
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
|
||||
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
|
||||
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
|
||||
glfw.window_hint(glfw.RESIZABLE, glfw.TRUE)
|
||||
glfw.window_hint(glfw.FOCUSED, glfw.TRUE)
|
||||
glfw.window_hint(glfw.FOCUS_ON_SHOW, glfw.TRUE)
|
||||
|
||||
window = glfw.create_window(WINDOW_WIDTH, WINDOW_HEIGHT, "Deblock Shader Testbed", None, None)
|
||||
if not window:
|
||||
glfw.terminate()
|
||||
print("ERROR: Failed to create window")
|
||||
sys.exit(1)
|
||||
|
||||
glfw.make_context_current(window)
|
||||
glfw.set_key_callback(window, key_callback)
|
||||
glfw.set_framebuffer_size_callback(window, framebuffer_size_callback)
|
||||
glfw.swap_interval(1) # VSync
|
||||
glfw.focus_window(window)
|
||||
|
||||
print(f"OpenGL: {glGetString(GL_VERSION).decode()}")
|
||||
|
||||
# Load shader (exit on failure at startup)
|
||||
g_state['program'] = load_shader(shader_path)
|
||||
if g_state['program'] is None:
|
||||
glfw.terminate()
|
||||
sys.exit(1)
|
||||
|
||||
# Load texture
|
||||
g_state['texture'], g_state['tex_size'] = load_mipmap_texture(mip_paths)
|
||||
set_filter_mode('BILINEAR')
|
||||
|
||||
# Create quad
|
||||
aspect = g_state['tex_size'][0] / g_state['tex_size'][1]
|
||||
g_state['quad_vao'] = create_quad(aspect)
|
||||
|
||||
# Create cube
|
||||
g_state['cube_vao'], g_state['cube_index_count'] = create_cube(1.0)
|
||||
|
||||
# Init debug rendering
|
||||
init_debug_rendering()
|
||||
|
||||
glEnable(GL_DEPTH_TEST)
|
||||
glClearColor(0.2, 0.2, 0.2, 1.0)
|
||||
|
||||
g_state['last_time'] = glfw.get_time()
|
||||
|
||||
# Main loop
|
||||
while not glfw.window_should_close(window):
|
||||
# Delta time
|
||||
now = glfw.get_time()
|
||||
dt = now - g_state['last_time']
|
||||
g_state['last_time'] = now
|
||||
|
||||
# Input
|
||||
glfw.poll_events()
|
||||
process_held_keys(window, dt)
|
||||
|
||||
# Projection matrix (recalculate for resize)
|
||||
proj = perspective_matrix(FOV_DEGREES, WINDOW_WIDTH / WINDOW_HEIGHT, 0.001, 100.0)
|
||||
|
||||
# Clear
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
||||
|
||||
# Draw quad or cube
|
||||
glUseProgram(g_state['program'])
|
||||
|
||||
# MVP (include rotation for cube mode)
|
||||
trans = translation_matrix(g_state['x'], g_state['y'], g_state['z'])
|
||||
rot_y = rotation_matrix_y(g_state['yaw'])
|
||||
rot_x = rotation_matrix_x(g_state['pitch'])
|
||||
model = trans @ rot_y @ rot_x
|
||||
mvp = proj @ model
|
||||
|
||||
loc = glGetUniformLocation(g_state['program'], "mvp")
|
||||
if loc >= 0:
|
||||
glUniformMatrix4fv(loc, 1, GL_TRUE, mvp)
|
||||
|
||||
loc = glGetUniformLocation(g_state['program'], "tex")
|
||||
if loc >= 0:
|
||||
glUniform1i(loc, 0)
|
||||
|
||||
loc = glGetUniformLocation(g_state['program'], "texSize")
|
||||
if loc >= 0:
|
||||
glUniform4f(loc, float(g_state['tex_size'][0]), float(g_state['tex_size'][1]), BLOCK_WIDTH, BLOCK_HEIGHT);
|
||||
|
||||
loc = glGetUniformLocation(g_state['program'], "const0")
|
||||
if loc >= 0:
|
||||
c = g_state['const0']
|
||||
glUniform4f(loc, c[0], c[1], c[2], c[3])
|
||||
|
||||
loc = glGetUniformLocation(g_state['program'], "const1")
|
||||
if loc >= 0:
|
||||
c = g_state['const1']
|
||||
glUniform4f(loc, c[0], c[1], c[2], c[3])
|
||||
|
||||
glActiveTexture(GL_TEXTURE0)
|
||||
glBindTexture(GL_TEXTURE_2D, g_state['texture'])
|
||||
|
||||
if g_state['mode'] == 'CUBE':
|
||||
glBindVertexArray(g_state['cube_vao'])
|
||||
glDrawElements(GL_TRIANGLES, g_state['cube_index_count'], GL_UNSIGNED_INT, None)
|
||||
else:
|
||||
glBindVertexArray(g_state['quad_vao'])
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None)
|
||||
|
||||
# Draw debug overlay
|
||||
draw_debug_text()
|
||||
|
||||
glfw.swap_buffers(window)
|
||||
|
||||
glfw.terminate()
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||