在做了 1~3 的基础工作后,我们的开发环境基本 OK 了,我们可以开始尝试利用 pyopengl 来进行绘制了。
本文主要有三个部分
- 利用 glfw 封装窗口类,并打开窗口;
- 封装 shader 类,进行编译、链接、使用;
- 封装 VAO、VBO、EBO
- 完成主函数进行绘制
完整的代码在仓库 (tag: v0.1) https://github.com/MangoWAY/CGLearner/tree/v0.1
1. 利用 glfw 封装窗口类
为了显示我们绘制的内容,打开窗口是必不可少的操作,因此我们来简单封装一个窗口类,便于我们后续的学习、调用。我们设置 opengl 的版本,向前兼容和配置(这俩在 macOS 必须配置),这些其实可以不用太关心,并不影响我们后续的学习进程,感兴趣可以看一下 glfw 的官方关于窗口的文档。
# window_helper.py import glfw, logging, sys from OpenGL import GL as gl log = logging.getLogger(__name__) class Window: class Config: def __init__(self,gl_version = (3,3), size = (500,400), title = "cglearn", bgcolor = (0,0.4,0)) -> None: self.gl_version = gl_version self.size = size self.title = title self.bgcolor = bgcolor def __init__(self,config: Config) -> None: self.native_window = None self.config = config self.init(config) def set_background(self,r,g,b): gl.glClearColor(r, g, b, 0) def init(self, config: Config): if not glfw.init(): log.error('failed to initialize GLFW') sys.exit(1) log.debug('requiring modern OpenGL without any legacy features') glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, config.gl_version[0]) glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, config.gl_version[1]) glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True) glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) log.debug('opening window') self.native_window = glfw.create_window(config.size[0], config.size[1], config.title, None, None) if not self.native_window: log.error('failed to open GLFW window.') sys.exit(2) glfw.make_context_current(self.native_window) log.debug('set background to dark blue') gl.glClearColor(0, config.bgcolor[0], config.bgcolor[1],config.bgcolor[2])
2. 封装 shader 类
用 OpenGL 完成一次简单的绘制有一些基本的操作,
- 需要编写 shader,然后创建 shader 程序,进行编译、链接、激活;
- 需要创建 VAO,VBO,EBO(可选),来管理数据,传递给 shader 进行计算;
- 在循环中调用绘制指令来进行绘制;
这一小节我们来封装一个 shader 类,来完成 shader 的创建、编译、链接等操作。
创建一个 shader 分几个步骤:
- 创建 VERTEX 和 FRAGMENT shader;
- 传送 shader 的代码 (string);
- 编译 VERTEX 和 FRAGMENT shader;
- 创建 program (shader 程序);
- 将 VERTEX 和 FRAGMENT shader 附加到 program 上;
- 链接 program;
在渲染前,还要激活 shader 程序
# shader.py import sys from OpenGL import GL as gl from enum import Enum import logging log = logging.getLogger(__name__) class ShaderType(Enum): VERTEX = 0 FRAGMENT = 1 class Shader: def __init__(self) -> None: self.vertex_shader = "" self.fragment_shader = "" self.program_id = -1 self.shader_ids = [] def load_shader_source_from_string(self, shader_type: ShaderType, source: str): if shader_type == ShaderType.VERTEX: self.vertex_shader = source elif shader_type == ShaderType.FRAGMENT: self.fragment_shader = source else: logging.error("wrong shader type !") # 从文件读取 shader,按照普通的文本文件读取即可。 def load_shader_source_from_path(self, shader_type: ShaderType, path: str): with open(path,"r") as f: source = f.read() self.load_shader_source_from_string(shader_type, source) # 这个主要是用来打印编译时候出现的错误信息,不是关键,这里先略去 def log_shader_info(self, shader_id): ... # 这个主要是用来打印链接时候出现的错误信息,不是关键,这里先略去 def log_program_info(self,program_id): ... def create_program(self): # 创建 shader 程序 self.program_id = gl.glCreateProgram() for shader_type in [gl.GL_VERTEX_SHADER, gl.GL_FRAGMENT_SHADER]: # 创建 VERTEX 和 FRAGMENT shader shader_id = gl.glCreateShader(shader_type) # 传送 shader 代码 if shader_type == gl.GL_VERTEX_SHADER: gl.glShaderSource(shader_id, self.vertex_shader) else: gl.glShaderSource(shader_id, self.fragment_shader) log.debug(f'compiling the {shader_type} shader') # 编译 VERTEX 和 FRAGMENT shader gl.glCompileShader(shader_id) self.log_shader_info(shader_id) # 将 VERTEX 和 FRAGMENT shader 附加到 program 上 gl.glAttachShader(self.program_id, shader_id) self.shader_ids.append(shader_id) log.debug('linking shader program') # 链接 shader 程序 gl.glLinkProgram(self.program_id) self.log_program_info(self.program_id) log.debug('installing shader program into rendering state') # 激活 shader 程序 def use_program(self): gl.glUseProgram(self.program_id) # 删除 shader 程序 def clean_program(self): log.debug('cleaning up shader program') for shader_id in self.shader_ids: gl.glDetachShader(self.program_id, shader_id) gl.glDeleteShader(shader_id) gl.glUseProgram(0) gl.glDeleteProgram(self.program_id)
3. 封装 VAO、VBO、EBO
VBO 一般用来存储顶点数据之类的信息,EBO 一般用来存储索引信息,VBO 和 EBO 都表现为 buffer,只不过类型不一样。OpenGL 是个巨大的状态机,需要各种设置状态,每次渲染前都要正确的 bind VBO、EBO 等等,这个时候 可以用 VAO 可以用来管理 VBO 和 EBO 等的信息,在后续绘制中,只要 bind VAO 即可,不用再 bind VBO、EBO 等,比较方便。关于 VAO、VBO、EBO 的详细说明,这里就不过多的解释了,网上有很多的资料,这里只是想展示它们的基本用法。
基本的操作顺序:
- 创建 VAO,bind VAO;
- 创建 VBO,bind VBO,传送 VBO 数据,设置顶点属性,启用顶点属性;
- 创建 EBO,bind EBO,传送 EBO 数据;
- unbind VAO、VBO、EBO
from OpenGL import GL as gl import logging, ctypes log = logging.getLogger(__name__) # 用来管理 VAO、VBO、EBO class RendererData: def __init__(self) -> None: self.vao: VAO = None self.vbo: VBO = None self.ebo: VBO = None def use(self): self.vao.bind() def unuse(self): self.vao.unbind() def draw(self): self.use() gl.glDrawElements(gl.GL_TRIANGLES, len(self.ebo.indices), gl.GL_UNSIGNED_INT, None) self.unuse() def build_data(self, desp:list, vertices:list, indices:list): # create vertex array object self.vao = VAO() self.vao.create_vertex_array_object() self.vao.bind() # create vertex buffer object self.vbo = VBO() self.vbo.vertex_attrib_desps = desp self.vbo.vertex_data = vertices self.vbo.create_vertex_array_object() self.vbo.bind() self.vbo.gen_buffer_data() # create element buffer object self.ebo = EBO() self.ebo.indices = indices self.ebo.create_index_array_object() self.ebo.bind() self.ebo.gen_buffer_data() # unbind all self.vao.unbind() self.vbo.unbind() self.ebo.unbind() def clean(self): self.vao.clean() self.vbo.clean() self.ebo.clean() class VAO: def __init__(self) -> None: self.vao_id = -1 def clean(self): log.debug('cleaning up vertex array') gl.glDeleteVertexArrays(1, [self.vao_id]) def create_vertex_array_object(self): log.debug('creating and binding the vertex array (VAO)') self.vao_id = gl.glGenVertexArrays(1) def bind(self): gl.glBindVertexArray(self.vao_id) def unbind(self): gl.glBindVertexArray(0) # 描述顶点数据的布局信息 class VertexAttribDesp: def __init__(self) -> None: self.attr_id = 0 self.comp_count = 3 self.comp_type = gl.GL_FLOAT self.need_nor = False self.stride = 0 self.offset = 0 class EBO: def __init__(self) -> None: self.indices = [] self.buffer_id = -1 def create_index_array_object(self): self.buffer_id = gl.glGenBuffers(1) def bind(self): gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.buffer_id) def unbind(self): gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0) def gen_buffer_data(self): array_type = (gl.GLuint * len(self.indices)) gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, len(self.indices) * ctypes.sizeof(ctypes.c_uint), array_type(*self.indices), gl.GL_STATIC_DRAW ) def clean(self): log.debug('cleaning up buffer') gl.glDeleteBuffers(1, [self.buffer_id]) class VBO: def __init__(self) -> None: self.vertex_data = [] self.vertex_attrib_desps = [] self.buffer_id = -1 def clean(self): log.debug('cleaning up buffer') for desp in self.vertex_attrib_desps: gl.glDisableVertexAttribArray(desp.attr_id) gl.glDeleteBuffers(1, [self.buffer_id]) def create_vertex_array_object(self): self.buffer_id = gl.glGenBuffers(1) def bind(self): gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.buffer_id) def unbind(self): gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) def gen_buffer_data(self): array_type = (gl.GLfloat * len(self.vertex_data)) gl.glBufferData(gl.GL_ARRAY_BUFFER, len(self.vertex_data) * ctypes.sizeof(ctypes.c_float), array_type(*self.vertex_data), gl.GL_STATIC_DRAW) log.debug('setting the vertex attributes') for desp in self.vertex_attrib_desps: gl.glVertexAttribPointer( desp.attr_id, desp.comp_count, desp.comp_type, desp.need_nor, desp.stride, desp.offset ) gl.glEnableVertexAttribArray(desp.attr_id)
4. 完成主函数进行绘制
from base import shader, window_helper from OpenGL import GL as gl from base.gl_render_data import * import glfw # ----- 创建窗口 window_config = window_helper.Window.Config(bgcolor = (0.5,0.5,0.5)) window = window_helper.Window(window_config) # ----- # ----- 创建着色器,从文件中读取 # base.vert """ #version 330 core layout(location = 0) in vec3 aPos; void main(){ gl_Position.xyz = aPos; gl_Position.w = 1.0; } """ # base.frag """ #version 330 core out vec3 color; void main(){ color = vec3(1,0,0); } """ mshader = shader.Shader() mshader.load_shader_source_from_path(shader.ShaderType.VERTEX, "shader/base.vert") mshader.load_shader_source_from_path(shader.ShaderType.FRAGMENT, "shader/base.frag") mshader.create_program() mshader.use_program() # ----- # ---- 创建 VAO、VBO、EBO,设置顶点属性 data = RendererData() desp = VertexAttribDesp() desp.attr_id = 0 desp.comp_count = 3 desp.stride = 3 * 4 desp.offset = None desp.need_nor = False desp.comp_type = gl.GL_FLOAT vert = [-0.5, 0.5, 0, 0.5, 0.5, 0, 0.5, -0.5, 0, -0.5, -0.5 ,0 ] inde = [ 3,1,0, 3,2,1 ] data.build_data([desp],vert,inde) data.use() # --------- # ----- 渲染循环 while ( glfw.get_key(window.native_window, glfw.KEY_ESCAPE) != glfw.PRESS and not glfw.window_should_close(window.native_window) ): gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) data.draw() glfw.swap_buffers(window.native_window) glfw.poll_events() # -----
最终可以渲染出一个红色的矩形。
5. 总结
- 利用 glfw 来管理窗口,glfw 做了两件事情,一件事是管理窗口,第二件是管理 OpenGL context,注意要正确设置
window_hint
; - 正确创建、编译、链接 shader;
- 正确创建和 bind VAO、VBO、EBO;
- 在主函数中创建相应的对象,在循环中渲染;