Renderizado con shaders¶
En este momento, para cada fotograma del escenario, la CPU debe recorrer y procesar individualmente cada uno de los vértices de los modelos.
Mediante el uso de shaders, en OpenGL podemos almacenar esos mismos vértices en la memoria de la tarjera gráfica y procesarlos sacando partido a su potencia, multiplicando exponencialmente la capacidad de renderizado.
Refactorizando el motor¶
Voy a crear una nueva versión del motor preparándolo para hacer uso de los shaders.
Un "vertex shader" es una pieza de código que define cómo debe la gráfica renderizar un conjunto de los vértices almacenados en el buffer de su memoria. Pero más allá de la posición en el espacio tridimensional, este "vertex shader" se complementa con un "fragment shader" que trabaja a nivel de píxel en el área de las mallas formadas por los vértices, y especifica entre otras cosas, cómo colorear ese espacio.
Empezaremos con la implementación de la App
:
engine/PyOGLApp.py
import sys
import pygame as pg
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
from .Camera import *
class PyOGLApp:
def __init__(self, title, width, height, maxFps):
pg.init()
# OpenGL Shaders Configurations
pg.display.gl_set_attribute(pg.GL_MULTISAMPLEBUFFERS, 1)
pg.display.gl_set_attribute(pg.GL_MULTISAMPLESAMPLES, 4)
pg.display.gl_set_attribute(pg.GL_CONTEXT_PROFILE_MASK, pg.GL_CONTEXT_PROFILE_CORE)
self.title, self.maxFps = title, maxFps
self.screenWidth, self.screenHeight = width, height
self.display = pg.display.set_mode((width, height), DOUBLEBUF | OPENGL)
self.clock = pg.time.Clock()
self.camera = Camera()
# Vertex Array Object reference
self.vao_ref = None
# Program Id
self.program_id = None
# Number of vertex to render
self.vertex_count = 0
def Init(self):
pass
def Inputs(self):
for event in self.events:
if event.type == MOUSEBUTTONDOWN and event.button == 1:
pg.mouse.set_pos(self.screenWidth / 2, self.screenHeight / 2)
pg.mouse.set_visible(False)
pg.event.set_grab(True)
if event.type == MOUSEBUTTONUP and event.button == 1:
pg.mouse.set_pos(self.screenWidth / 2, self.screenHeight / 2)
pg.mouse.set_visible(True)
pg.event.set_grab(False)
def Update(self):
self.CameraInit()
def Render(self):
pass
def Run(self):
self.Init() # initial members
while 1:
self.events = pg.event.get()
for event in self.events:
if event.type == pg.QUIT:
sys.exit()
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
sys.exit()
self.deltaTime = self.clock.tick(self.maxFps) / 1000
self.Inputs() # frame inputs
self.Update() # frame logic
self.Render() # frame drawing
pg.display.flip()
pg.display.set_caption(
f"{self.title} ({self.clock.get_fps():.2f} fps)")
def CameraInit(self):
pass
def WorldAxes(self):
# Dibujamos las líneas para los ejes
glLineWidth(3)
glBegin(GL_LINES)
glColor(1, 0, 0)
glVertex3d(-100, 0, 0) # Eje X
glVertex3d(100, 0, 0)
glColor(0, 1, 0)
glVertex3d(0, -100, 0) # Eje Y
glVertex3d(0, 100, 0)
glColor(0, 0, 1)
glVertex3d(0, 0, -100) # Eje Z
glVertex3d(0, 0, 100)
glEnd()
# Creamos un objeto cuádrico en OpenGL para una esfera
# https://docs.microsoft.com/es-es/windows/win32/opengl/glunewquadric
sphere = gluNewQuadric()
# Dibujamos una esfera para el eje X
glColor(1, 0, 0)
glPushMatrix()
glTranslated(1, 0, 0)
gluSphere(sphere, 0.05, 10, 10)
glPopMatrix()
# Dibujamos una esfera para el eje Y
glColor(0, 1, 0)
glPushMatrix()
glTranslated(0, 1, 0)
gluSphere(sphere, 0.05, 10, 10)
glPopMatrix()
# Dibujamos una esfera para el eje Z
glColor(0, 0, 1)
glPushMatrix()
glTranslated(0, 0, 1)
gluSphere(sphere, 0.05, 10, 10)
glPopMatrix()
glColor(1, 1, 1)
glLineWidth(1)
Ésta tomará la configuración de la clase Camera
:
engine/Camera.py
import pygame as pg
from OpenGL.GLU import *
from math import *
class Camera:
def __init__(self):
# Posición del ojo (origen)
self.eye = pg.math.Vector3(0, 0, 0)
# Vectores de dirección Y, X, Z
self.up = pg.math.Vector3(0, 1, 0)
self.right = pg.math.Vector3(1, 0, 0)
self.forward = pg.math.Vector3(0, 0, 1)
# Dirección hacia donde mira la cámara (adelante)
self.look = self.eye + self.forward
# Rotaciones de la cámara a partir del ratón
self.mouseSensitivity = pg.math.Vector2(10, 15)
self.lastMouse = pg.math.Vector2(300, 300)
self.pitch = 0
self.yaw = -90
# Valor inicial del vector forward normalizado
self.forward.x = cos(radians(self.yaw)) * cos(radians(self.pitch))
self.forward.y = sin(radians(self.pitch))
self.forward.z = sin(radians(self.yaw)) * cos(radians(self.pitch))
self.forward = self.forward.normalize()
def Rotate(self, yaw, pitch):
# Incrementamos los ángulos de rotación
self.pitch += pitch
self.yaw += yaw
if self.pitch > 89.0:
self.pitch = 89
if self.pitch < -89.0:
self.pitch = -89
# Aplicamos las rotaciones mediante trigonometría
self.forward.x = cos(radians(self.yaw)) * cos(radians(self.pitch))
self.forward.y = sin(radians(self.pitch))
self.forward.z = sin(radians(self.yaw)) * cos(radians(self.pitch))
# Normalizamos el vector adelante
self.forward = self.forward.normalize()
# Recalculamos el vector derecho haciendo el producto vectorial
self.right = self.forward.cross(pg.Vector3(0, 1, 0)).normalize()
# Recalculamos el vector arriba haciendo el producto vectorial
self.up = self.right.cross(self.forward).normalize()
def Update(self, deltaTime=1, screenWidth=1, screenHeight=1):
if not pg.mouse.get_visible():
# Procesamiento de la dirección
mouseChange = self.lastMouse - pg.math.Vector2(pg.mouse.get_pos())
pg.mouse.set_pos(screenWidth / 2, screenHeight / 2)
self.Rotate(-mouseChange.x * self.mouseSensitivity.x * deltaTime,
mouseChange.y * self.mouseSensitivity.y * deltaTime)
self.lastMouse = pg.mouse.get_pos()
# Precesamiento del movimiento
keys = pg.key.get_pressed()
# Si presionamos la letra W moveremos la cámara adelante
if keys[pg.K_w]:
self.eye += self.forward * deltaTime
# Si presionamos la letra S moveremos la cámara atrás
if keys[pg.K_s]:
self.eye -= self.forward * deltaTime
# Si presionamos la letra D moveremos la cámara a la derecha
if keys[pg.K_d]:
self.eye += self.right * deltaTime
# Si presionamos la letra A moveremos la cámara a la izquierda
if keys[pg.K_a]:
self.eye -= self.right * deltaTime
# Si presionamos el espacio moveremos la cámara hacia arriba
if keys[pg.K_SPACE]:
self.eye += self.up * deltaTime
# Si presionamos el ALT izquierdo moveremos la cámara abajo
if keys[pg.K_LALT]:
self.eye -= self.up * deltaTime
# Actualizamos la dirección a donde mira la cámara
self.look = self.eye + self.forward
# Estableceremos la transformación de visualización
# https://docs.microsoft.com/eu-es/windows/win32/opengl/glulookat
gluLookAt(self.eye.x, self.eye.y, self.eye.z,
self.look.x, self.look.y, self.look.z,
self.up.x, self.up.y, self.up.z)
Para hacer uso de nuestro motor crearemos una instancia de la aplicación:
refactortest.py
import sys
sys.path.append('..')
from res.engine.PyOGLApp import *
class RefactorTest(PyOGLApp):
def Init(self):
bgColor = (0, 0, 0, 1)
drawingColor = (1, 1, 1, 1)
glClearColor(bgColor[0], bgColor[1], bgColor[2], bgColor[3])
glColor(drawingColor)
# Projection
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(60, (self.screenWidth / self.screenHeight), 0.1, 1000.0)
def CameraInit(self):
# Model View
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
glViewport(0, 0, self.screenWidth, self.screenHeight)
glEnable(GL_DEPTH_TEST)
# Camera Updating
self.camera.Update(self.deltaTime, self.screenWidth, self.screenHeight)
def Render(self):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
self.WorldAxes()
if __name__ == '__main__':
app = RefactorTest("OpenGL en Python", 600, 600, 60)
app.Run()
Hasta aquí sin más, tenemos lo mismo que anteriormente.
Compilador de shaders¶
Para poder utilizar un shader debemos compilarlo, este proceso podemos programarlo en una función de utilidades:
engine/Utils.py
from OpenGL.GL import *
def compile_shader(shader_type, shader_source):
# Create a shader object
shader_id = glCreateShader(shader_type)
# Replace the source code in a shader object
glShaderSource(shader_id, shader_source)
# Compile a shader object
glCompileShader(shader_id)
# Return a parameter from a shader object
compile_success = glGetShaderiv(shader_id, GL_COMPILE_STATUS)
# If anything fails...
if not compile_success:
# Return the information log for a shader object
error_message = "\n" + glGetShaderInfoLog(shader_id)
# Delete a shader object
glDeleteShader(shader_id)
# Invoke an exception with the error
raise Exception(error_message)
# Finally return the compiled shader id
return shader_id
Los shaders compilados forman parte del programa que se ejecutará en la gráfica, tenemos que crear uno y asignarle los shaders de vértices y fragmentos:
engine/Utils.py
def create_program(vertex_shader_code, fragment_shader_code):
# Return the compiled vertex shader id
vertex_shader_id = compile_shader(GL_VERTEX_SHADER, vertex_shader_code)
# Return the compiled fragment shader id
frag_shader_id = compile_shader(GL_FRAGMENT_SHADER, fragment_shader_code)
# Create the program object
program_id = glCreateProgram()
# Attach the vertex shader object to the program object
glAttachShader(program_id, vertex_shader_id)
# Attach the fragment shader object to the program object
glAttachShader(program_id, frag_shader_id)
# Link the program object to process it
glLinkProgram(program_id)
# Return a parameter from a program object
link_success = glGetProgramiv(program_id, GL_LINK_STATUS)
# If anything fails...
if not link_success:
# Return the information log for a program object
info = glGetProgramInfoLog(program_id)
# Invoke an exception with the error
raise RuntimeError(info)
# If all its fine delete the vertex shader object
glDeleteShader(vertex_shader_id)
# And the fragment shader object
glDeleteShader(frag_shader_id)
# Finally return the compiled program id
return program_id
Nuestro primer shader¶
Ahora que tenemos el motor preparado para procesar shaders veamos como implementarlos. Teniendo en cuenta que un shader es una pieza de código tenemos la posibilidad de crearlos en un fichero de texto o directamente como una variable, por ahora vamos a por la segunda opción.
Empecemos con algo muy simple, dibujar un único vértice con un color en la pantalla:
import sys
from venv import create
import numpy as np
sys.path.append('..')
from res.engine.PyOGLApp import *
from res.engine.Utils import *
vertex_shader = r"""
#version 330 core
void main()
{
// posición del vértice de prueba
gl_Position = vec4(0.5, 0, 0, 1);
}
"""
fragment_shader = r"""
#version 330 core
out vec4 frag_color;
void main()
{
// color del vértice de prueba
frag_color = vec4(0, 1, 0, 1);
}
"""
class MyApp(PyOGLApp):
def Init(self):
# Creamos el programa pasándole ambos shaders
self.program_id = create_program(vertex_shader, fragment_shader)
# Generate a Vertex Array Object names
self.vao_ref = glGenVertexArrays(1)
# Bind a vertex array object
glBindVertexArray(self.vao_ref)
# Specify the diameter of rasterized points
glPointSize(10)
def Render(self):
# Clear buffers to preset values
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# Install a program object as part of current rendering state
glUseProgram(self.program_id)
# Render primitives from array data
glDrawArrays(GL_POINTS, 0, 1)
if __name__ == '__main__':
app = MyApp("Primer Shader", 600, 600, 60)
app.Run()
Shader con múltiples vértices¶
Para procesar más de un vértice debemos almacenarlos en el buffer de la gráfica. Almacenar datos significa que necesitamos definirlos y empaquetarlos en variables y para ello haremos uso de nuestra propia clase GraphicsData
:
engine/GraphicsData.py
from OpenGL.GL import *
import numpy as np
class GraphicsData:
def __init__(self, data_type, data):
# Store the data
self.data = data
# Set the data type
self.data_type = data_type
# Generate a Buffer Object Names
self.buffer_ref = glGenBuffers(1)
# Load the data
self.load()
def load(self):
# Create an array of float32 type to represent positions
data = np.array(self.data, np.float32)
# Bind the named buffer object
glBindBuffer(GL_ARRAY_BUFFER, self.buffer_ref)
# Creates and initialize a buffer object's data store
# data.ravel() -> toma los datos del array de numpy y lo transforma a
# un array unidimensional de números, que es lo que espera OpenGL
glBufferData(GL_ARRAY_BUFFER, data.ravel(), GL_STATIC_DRAW)
def create_variable(self, program_id, variable_name):
# Return the location of an attribute variable
variable_id = glGetAttribLocation(program_id, variable_name)
# Bind the named buffer object
glBindBuffer(GL_ARRAY_BUFFER, self.buffer_ref)
# If data is a vector3...
if self.data_type == "vec3":
# Define an array of generic vertex attribute data
# 3 -> Size
# GL_FLOAT -> Type
# False -> Not normalized
# 0 -> Stride size
# None -> Mixed data pointer
glVertexAttribPointer(variable_id, 3, GL_FLOAT, False, 0, None)
# Enable or disable a generic vertex attribute array
glEnableVertexAttribArray(variable_id)
Ahora ya podemos definir una lista de múltiples vértices en nuestra aplicación en porciones de 3 números (las posiciones x, y, z):
import sys
import numpy as np
sys.path.append('..')
from res.engine.PyOGLApp import *
from res.engine.GraphicsData import *
from res.engine.Utils import *
vertex_shader = r"""
#version 330 core
// Variable que toma datos externos
in vec3 position;
void main()
{
// posición del vértice tomado del exterior
gl_Position = vec4(position.x, position.y, position.z, 1);
}
"""
fragment_shader = r"""
#version 330 core
out vec4 frag_color;
void main()
{
// color del vértice de prueba
frag_color = vec4(0, 1, 0, 1);
}
"""
class MyApp(PyOGLApp):
def Init(self):
self.program_id = create_program(vertex_shader, fragment_shader)
self.vao_ref = glGenVertexArrays(1)
glBindVertexArray(self.vao_ref)
glPointSize(10)
# Define multiple vertices
position_data = [
[0, -.9, 0],
[-.6, .8, 0],
[.9, -.2, 0],
[-.9, -.2, 0],
[.6, .8, 0]
]
# Save the number of vertices
self.vertex_count = len(position_data)
# Define the variable for the vec3 chunks using the vertices list
position_variable = GraphicsData("vec3", position_data)
# Create the variable in the memory buffer
position_variable.create_variable(self.program_id, "position")
def Render(self):
# Clear buffers to preset values
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# Install a program object as part of current rendering state
glUseProgram(self.program_id)
# Render primitives from array data passing the counter
glDrawArrays(GL_POINTS, 0, self.vertex_count)
if __name__ == '__main__':
app = MyApp("Segundo Shader", 600, 600, 60)
app.Run()
Estrella de colores¶
En este tercer shader vamos a utilizar una segunda lista con colores para los puntos, deberemos crear la variable y almacenarla en el buffer.
Respecto al "fragment shader", éste toma como entradas las salidas del "vertex shader", razón por la cuál tendremos un out vec3 color;
que retomaremos in vec3 color;
:
import sys
from venv import create
import numpy as np
sys.path.append('..')
from res.engine.PyOGLApp import *
from res.engine.GraphicsData import *
from res.engine.Utils import *
vertex_shader = r"""
#version 330 core
// Variable que toma datos externos
in vec3 position;
// Variable con un color para el fragment shader
in vec3 vertex_color;
out vec3 color;
void main()
{
// posición del vértice tomado del exterior
gl_Position = vec4(position.x, position.y, position.z, 1);
// Devolvemos el mismo color
color = vertex_color;
}
"""
fragment_shader = r"""
#version 330 core
in vec3 color;
out vec4 frag_color;
void main()
{
// color del vértice de prueba
frag_color = vec4(color, 1);
}
"""
class MyApp(PyOGLApp):
def Init(self):
self.program_id = create_program(vertex_shader, fragment_shader)
self.vao_ref = glGenVertexArrays(1)
glBindVertexArray(self.vao_ref)
# Variable que controla las posiciones
position_data = [
[0, -.8, 0],
[-.6, .8, 0],
[.8, -.2, 0],
[-.8, -.2, 0],
[.6, .8, 0]
]
position_variable = GraphicsData("vec3", position_data)
position_variable.create_variable(self.program_id, "position")
# Variable que controla los colores
color_data = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
[1, 0, 1],
[1, 1, 0]
]
color_variable = GraphicsData("vec3", color_data)
color_variable.create_variable(self.program_id, "vertex_color")
# Save the number of vertices
self.vertex_count = len(position_data)
def Render(self):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glUseProgram(self.program_id)
glDrawArrays(GL_LINE_LOOP, 0, self.vertex_count)
if __name__ == '__main__':
app = MyApp("Tercer Shader", 600, 600, 60)
app.Run()
Renderizando un modelo¶
Nuestro siguiente objetivo es dibujar un modelo mediante shaders y para ello definiremos nuestra propia implementación de la clase Mesh
que podremos extender para crear diferentes formas:
engine/Mesh.py
from OpenGL import *
from .GraphicsData import *
from .Uniform import *
import pygame as pg
import numpy as np
class Mesh:
def __init__(self, program_id, vertices, vertex_colors, draw_type):
# Lista con los vértices
self.vertices = vertices
# Tipo de dibujado
self.draw_type = draw_type
# Creamos un VAO para el mesh
self.vao_ref = glGenVertexArrays(1)
# Enlazamos el VAO del mesh
glBindVertexArray(self.vao_ref)
# Creamos una variable para la posición de los vértices
position = GraphicsData("vec3", self.vertices)
position.create_variable(program_id, "position")
# Creamos una variable para el color de los vértices
colors = GraphicsData("vec3", vertex_colors)
colors.create_variable(program_id, "vertex_color")
def draw(self):
# Enlazamos el VAO del mesh
glBindVertexArray(self.vao_ref)
# Dibujamos el VAO
glDrawArrays(self.draw_type, 0, len(self.vertices))
Esta clase podemos extenderla en nuestras propias clases Triangle
y Square
sin complicación:
engine/Triangle.py
from .Mesh import *
class Triangle(Mesh):
def __init__(self, program_id, location=None):
vertices = [
[0, 0.25, -1.0],
[0.5, -0.5, -1.0],
[-0.5, -0.5, -1.0]
]
colors = [
[0, 1, 1],
[0, 1, 0],
[0, 0.5, 0.5]
]
super().__init__(program_id, vertices, colors, GL_TRIANGLE_FAN, location)
engine/Square.py
from .Mesh import *
class Square(Mesh):
def __init__(self, program_id, location=None):
vertices = [
[0.5, 0.5, -1.0],
[0.5, -0.5, -1.0],
[-0.5, -0.5, -1.0],
[-0.5, 0.5, -1.0]
]
colors = [
[1, 0, 0],
[1.0, 0.5, 0],
[1, 1, 0],
[0, 1, 0]
]
super().__init__(program_id, vertices, colors, GL_TRIANGLE_FAN, location)
Y ya podremos hacer uso de ellas en nuestra aplicación:
import sys
from venv import create
import numpy as np
sys.path.append('..')
from res.engine.PyOGLApp import *
from res.engine.Utils import *
from res.engine.Square import *
from res.engine.Triangle import *
from res.engine.GraphicsData import *
vertex_shader = r"""
#version 330 core
// Variable que toma datos externos
in vec3 position;
// Variable con un color para el fragment shader
in vec3 vertex_color;
out vec3 color;
void main()
{
// posición del vértice tomado del exterior
gl_Position = vec4(position.x, position.y, position.z, 1);
// Devolvemos el mismo color
color = vertex_color;
}
"""
fragment_shader = r"""
#version 330 core
in vec3 color;
out vec4 frag_color;
void main()
{
// color del vértice de prueba
frag_color = vec4(color, 1);
}
"""
class MyApp(PyOGLApp):
def Init(self):
# Creamos el programa con los shaders
self.program_id = create_program(vertex_shader, fragment_shader)
# Creamos un cuadrado y un triángulo
self.square = Square(self.program_id)
self.triangle = Triangle(self.program_id)
def Render(self):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glUseProgram(self.program_id)
# Dibujamos ambas formas de abajo hacia arriba
self.square.draw()
self.triangle.draw()
if __name__ == '__main__':
app = MyApp("Cuarto Shader", 600, 600, 60)
app.Run()
Traslación en el modelo¶
Por último en esta sección veamos cómo enviar un movimiento de traslación a la hora de dibujar un modelo con los shaders.
Como debemos representar la traslación en una posición necesitaremos almacenarla en la gráfica y hacer uso de ella en los shaders. Con ese objetivo crearemos una implementación de la clase Uniform
que nos permitirá crear variables y buscarlas cómodamente.
A uniform is a global Shader variable declared with the "uniform" storage qualifier. These act as parameters that the user of a shader program can pass to that program. Their values are stored in a program object.
engine/Uniform.py
from OpenGL.GL import *
class Uniform:
def __init__(self, data_type, data):
self.variable_id = None
self.data = data
self.data_type = data_type
def find_variable(self, program_id, variable_name):
# Return the location of a uniform variable
self.variable_id = glGetUniformLocation(program_id, variable_name)
def load(self):
if self.data_type == "vec3":
# Set the value of a uniform variable for the current program object
glUniform3f(
self.variable_id,
self.data[0],
self.data[1],
self.data[2]
)
Estableceremos una traslación para modificar el offset del centro del modelo, por eso lo podemos pasar como un pg.Vector3
al crearlo. En caso de que lo establezcamos crearemos la variable uniform y la cargaremos en el shader antes de realizar el dibujado:
engine/Mesh.py
from OpenGL import *
from .GraphicsData import *
from .Uniform import *
import pygame as pg
import numpy as np
class Mesh:
def __init__(self, program_id, vertices, vertex_colors, draw_type, translation=None):
self.vertices = vertices
self.draw_type = draw_type
self.vao_ref = glGenVertexArrays(1)
glBindVertexArray(self.vao_ref)
position = GraphicsData("vec3", self.vertices)
position.create_variable(program_id, "position")
colors = GraphicsData("vec3", vertex_colors)
colors.create_variable(program_id, "vertex_color")
# Si pasamos una posición de traslación inicial
if translation:
# Creamos la variable en el programa
self.translation = Uniform("vec3", translation)
self.translation.find_variable(program_id, "translation")
def draw(self):
# Si tenemos la traslación la cargamos en el shader
if self.translation:
self.translation.load()
# Luego realizamos el dibujado de forma normal
glBindVertexArray(self.vao_ref)
glDrawArrays(self.draw_type, 0, len(self.vertices))
La clave es tomar esta cantidad traslación como una variable unform en el "vertex shader" y sumarla a la posición actual:
import sys
from venv import create
import numpy as np
sys.path.append('..')
from res.engine.PyOGLApp import *
from res.engine.Utils import *
from res.engine.Square import *
from res.engine.Triangle import *
from res.engine.GraphicsData import *
vertex_shader = r"""
#version 330 core
// Variable que toma datos externos
in vec3 position;
// Variable con un color para el fragment shader
in vec3 vertex_color;
out vec3 color;
// Variable para manejar una traslación
uniform vec3 translation;
void main()
{
// Sumamos la posición trasladada a la actual
vec3 pos = position + translation;
// Posición del vértice tomado del exterior
gl_Position = vec4(pos, 1);
// Devolvemos el mismo color
color = vertex_color;
}
"""
fragment_shader = r"""
#version 330 core
in vec3 color;
out vec4 frag_color;
void main()
{
// color del vértice de prueba
frag_color = vec4(color, 1);
}
"""
class MyApp(PyOGLApp):
def Init(self):
self.program_id = create_program(vertex_shader, fragment_shader)
# Creamos el modelo pasando un vector de traslación inicial
self.square = Square(self.program_id, pg.Vector3(-0.5, 0.5, 0))
self.triangle = Triangle(self.program_id, pg.Vector3(0.5, -0.5, 0))
def Render(self):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glUseProgram(self.program_id)
self.square.draw()
self.triangle.draw()
if __name__ == '__main__':
app = MyApp("Quinto Shader", 600, 600, 60)
app.Run()
Como vemos estamos renderizando dos modelos, pero en lugar de en cada iteración realizar el proceso de dibujado de los vértices, los almacenamos en la memoria de la tarjeta gráfica en una referencia VAO (Vertex Array Object) y simplemente los dibujamos.
Este concepto de "guardar" información en la memoria de la gráfica y utilizarla cuando lo necesitemos es muy conveniente y abre la puerta a realizar innumerables experimentos, permitiéndose renderizar complejos modelos tridimensionales con un lenguaje poco potente como es Python.
Última edición: 12 de Octubre de 2022