PyOpenGL代码实战(四):着色器

一、理论基础

我们在上一章已经介绍过着色器。着色器主要有顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)两种。

实际上,着色器程序并不只有这两种,还有几何着色器、曲面细分着色器等。因为应用较少,此处不再介绍。

顶点着色器:对顶点进行处理(三角形中的每一个顶点都会运行一次顶点着色器程序)。主要用于将顶点的模型坐标转换为NDC坐标

片元着色器:对像素进行处理(三角形中的每一个像素都会运行一次片元着色器程序)。主要用于像素的着色

二、着色器编程

在OpenGL中,着色器是由一门名为GLSL的编程语言写成的。GLSL是一种与C语言十分相似的GPU编程语言。

着色器还可以使用 HLSL 编写,在 Unity 中常用。

本文主要以上一章提供的着色器代码为例。

顶点着色器:

#version 330 corein vec3 aPos;
in vec3 aColor;
out vec3 VertexColor;void main()
{gl_Position = vec4(aPos, 1.0f);VertexColor = aColor;
}

片元着色器:

#version 330 corein vec3 VertexColor;
out vec4 FragColor;void main()
{FragColor = vec4(VertexColor, 1.0f);
} 

1、变量

1)变量类型

在GLSL中有以下类型的变量:

类型说明
int整型数
float浮点数
bool布尔类型
vecX浮点型向量,X表示元素个数(X可取2,3,4)
ivecX整数型向量
bvecX布尔型向量
matXX×X的浮点型矩阵
sampler2D2D纹理
samplerCube立方体纹理

上面的表格中,列出了GLSL的所有变量类型,GLSL中的大部分数据类型与其它编程语言无异,sampler2DsamplerCube将在后续章节中详细介绍。

除此之外,GLSL支持数组与结构体类型的变量,语法与C语言完全一致,此处不再介绍。

2)变量的传递

GLSL中的变量,可以在CPU、顶点着色器、片元着色器之间传递。传递变量的语法如下:

in vec3 aPos;
out vec4 FragColor;
uniform mat4 m;

在上面的代码中,in/out/uniform是用于表示这个变量是由谁传递来的。

关键字在顶点着色器中的含义在片元着色器中的含义
in该变量由CPU(通过顶点数据)传入该变量由顶点着色器传入
out该变量需传递给片元着色器固定用于out vec4 FragColor,设置像素颜色
uniform统一变量。该变量由CPU(在CPU代码中设置)传入。对于所有顶点,该变量的值相同同顶点着色器

在例子中,顶点着色器定义变量的代码如下:

// aPos和aColor都是由CPU通过顶点数据传入的
in vec3 aPos;
in vec3 aColor;
// VertexColor需要传递给片元着色器
out vec3 VertexColor;

片元着色器定义变量的代码如下:

// VertexColor是由顶点着色器传入的变量,注意此变量应与顶点着色器的相应变量同名同类型
in vec3 VertexColor;
// 固定写法,后续代码设置FragColor的值以设置像素颜色
out vec4 FragColor;

上面的代码只是定义了变量的传递方式,下面介绍如何真正传递变量的值

首先,CPU通过顶点数据向顶点着色器传递数据。这一部分的内容在上一章已经做过介绍。使用VBO传递顶点数据,使用glVertexAttribPointer将顶点数据分配给对应的变量。

# 坐标
aPosLoc = glGetAttribLocation(program, 'aPos')
glVertexAttribPointer(aPosLoc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(0))
glEnableVertexAttribArray(aPosLoc)
# 颜色
aColorLoc = glGetAttribLocation(program, 'aColor')
glVertexAttribPointer(aColorLoc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(12))
glEnableVertexAttribArray(aColorLoc)

注意,顶点着色器的每一个in变量,都会有一个索引,可以通过以下语法设置索引:

in vec3 aColor;	// 未设置索引,编译器自动分配索引
layout (location = 0) in vec3 aPos;	// 手动设置索引

如果没有设置索引,则编译器会自动为变量分配索引。

通过glGetAttribLocation(program, name)获取变量的索引值,通过索引值就可以将顶点数据与对应的变量绑定了。

顶点着色器向片元着色器传递变量,只需要在顶点着色器中用in修饰变量,在片元着色器中用out修饰变量,保证对应变量同名同类型即可。值得注意的是,顶点着色器是针对顶点而言的,而片元着色器是针对像素的,所有由顶点着色器传递给片元着色器的变量,会由GPU自动完成插值处理。

最后,是CPU通过设置统一变量来给两种着色器程序设置变量值

在GLSL中定义统一变量:

uniform vec3 color;

在Python代码中设置统一变量的值:

loc = glGetUniformLocation(program, "color")
glUniform3f(loc, 1, 0, 0)

和顶点着色器中的in变量一样,每个uniform变量也有一个唯一的索引,通过glGetUniformLocation(program, name)可以获取对应变量的索引,再通过一系列形如glUniformX(loc, *args)的函数来设置变量的值。其中X表示要设置的变量的长度和类型。

X含义
ffloat
iint
uiunsigned int
3fvec3,参数以3个float类型的变量的形式传递
3fvvec3,参数以数组的形式传递

2、功能实现

1)顶点着色器
#version 330 core	// 指定GLSL版本
// 定义变量
in vec3 aPos;
in vec3 aColor;
out vec3 VertexColor;
// 主函数
void main()
{gl_Position = vec4(aPos, 1.0);	// 设置顶点坐标VertexColor = aColor;	// 设置要传给片元着色器的变量的值
}

注意:代码中的注释仅为说明作用,请不要在 GLSL 中输入中文(包括在注释中)。

在这段代码里,第一行用于指定GLSL版本为3.30,如果没有这一句代码,则编译器默认使用GLSL1.1版本,编译会报错。

在主函数中,有这样一句代码:

gl_Position = vec4(aPos, 1.0f);	// 设置顶点坐标

其中gl_Position是GLSL自带的变量,它用于设置顶点的NDC坐标。

注意到这里的gl_Position是一个4维向量,而 3D 空间中的坐标应该是三维的。因为这里的坐标是齐次坐标,GPU会自动将坐标的前三个分量除以最后一个分量,得到的三维向量即为 NDC 坐标。

2)片元着色器
#version 330 core
// 定义变量
in vec3 VertexColor;
out vec4 FragColor;
// 主函数
void main()
{FragColor = vec4(VertexColor, 1.0f);	// 设置像素颜色
} 

这段代码中,有这样一句代码值得注意:

FragColor = vec4(VertexColor, 1.0f);	// 设置像素颜色

FragColor变量是用户在前面定义的out变量,用于设置像素的颜色。

三、示例

接下来,我们将修改我们在前一章提供的着色器代码,来实现三角形时隐时现的效果:

在这里插入图片描述

由于gif的压缩原理,上图可能有些失真。

这个效果的实现非常简单,我们原来在片元着色器中有这样一段代码:

FragColor = vec4(VertexColor, 1.0f);

这段代码可以设置像素的颜色,我们只需要将VertexColor乘上一个随时间周期性变化的变量t,就可以实现这种效果:

// 首先在主函数之外定义uniform变量
uniform float t;
// ...
// 修改主函数的代码
FragColor = vec4(VertexColor * t, 1.0f);

接下来,我我们要通过Python传入t的值。

import math
from time import timeglUseProgram(program) # 使用着色器
tLoc = glGetUniformLocation(program, "t")
glUniform1f(tLoc, (math.sin(time()) + 1) / 2)

注意这段代码应该写在渲染函数里,因为t的值每次渲染都会发生变化。

至此,实现这一功能的代码已经完成。完整代码如下:

from time import timefrom OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.arrays.vbo import VBO
from csdn.window import Window
import numpy as npw = Window(1920, 1080, "Test")triangle = np.array([-0.5, -0.5, 0, 1, 0, 0,0.5, -0.5, 0, 0, 1, 0,0, 0.5, 0, 0, 0, 1
], dtype=np.float32)vao = glGenVertexArrays(1)
glBindVertexArray(vao)vbo = VBO(triangle, GL_STATIC_DRAW)
vbo.bind()vs = """
#version 330 core
in vec3 aPos;
in vec3 aColor;out vec3 VertexColor;void main()
{gl_Position = vec4(aPos, 1.0f);VertexColor = aColor;
}
"""fs = """
#version 330 core
in vec3 VertexColor;uniform float t;out vec4 FragColor;void main()
{FragColor = vec4(VertexColor * t, 1.0f);
} 
"""vsProgram = shaders.compileShader(vs, GL_VERTEX_SHADER)
fsProgram = shaders.compileShader(fs, GL_FRAGMENT_SHADER)
program = shaders.compileProgram(vsProgram, fsProgram)aPosLoc = glGetAttribLocation(program, 'aPos')
glVertexAttribPointer(aPosLoc, 3, GL_FLOAT, False, 24, ctypes.c_void_p(0))
glEnableVertexAttribArray(aPosLoc)aColorLoc = glGetAttribLocation(program, 'aColor')
glVertexAttribPointer(aColorLoc, 3, GL_FLOAT, False, 24, ctypes.c_void_p(12))
glEnableVertexAttribArray(aColorLoc)def render():# 此处传入t的值glUseProgram(program)tLoc = glGetUniformLocation(program, "t")glUniform1f(tLoc, (np.sin(time()) + 1) / 2)glBindVertexArray(vao)glDrawArrays(GL_TRIANGLES, 0, 3)w.loop(render)

四、封装

接下来,我们将把与着色器有关的代码封装成Shader类,以提高代码的复用性和可扩展性。

from OpenGL.GL import shaders
from OpenGL.GL import *
import numpy as npclass Shader:def __init__(self, vsPath, fsPath) -> None:""" 读取 GLSL 文件并编译 """with open(vsPath, 'r') as f:text = f.read()vs = shaders.compileShader(text, GL_VERTEX_SHADER)with open(fsPath, 'r') as f:text = f.read()fs = shaders.compileShader(text, GL_FRAGMENT_SHADER)self.shader = shaders.compileProgram(vs, fs)def use(self):glUseProgram(self.shader)def setUniform(self, name, value):""" 根据传入的数据类型,自动选择 glUniformX 函数 """self.use()loc = glGetUniformLocation(self.shader, name)dtype = type(value)if dtype == np.ndarray:size = value.sizedtype = value.dtypefuncs = {np.int32: [glUniform1i, glUniform2i, glUniform3i, glUniform4i],np.uint: [glUniform1ui, glUniform2ui, glUniform3ui, glUniform4ui],np.float32: [glUniform1f, glUniform2f, glUniform3f, glUniform4f],np.double: [glUniform1d, glUniform2d, glUniform3d, glUniform4d],}func = funcs[dtype][size - 1]func(loc, *value)returnelif dtype == int or dtype == np.int32 or dtype == np.int64:glUniform1i(loc, value)elif dtype == float or dtype == np.float64 or dtype == np.float32:glUniform1f(loc, value)else:raise RuntimeError("未知的参数类型!")def setAttrib(self, name, size, dtype, stride, offset):""" 设置顶点属性链接 """loc = glGetAttribLocation(self.shader, name)glVertexAttribPointer(loc, size, dtype, False, stride, ctypes.c_void_p(offset))glEnableVertexAttribArray(loc)

封装完成后,示例代码:

import math
from time import timeimport numpy as np
from OpenGL.GL import *
from OpenGL.arrays.vbo import VBO# 导入Window类和Shader类
from shader import Shader
from window import Windoww = Window(1920, 1080, "Test")triangle = np.array([-0.5, -0.5, 0, 1, 0, 0,0.5, -0.5, 0, 0, 1, 0,0, 0.5, 0, 0, 0, 1
], dtype=np.float32)vao = glGenVertexArrays(1)
glBindVertexArray(vao)vbo = VBO(triangle, GL_STATIC_DRAW)
vbo.bind()# 导入顶点着色器文件和片元着色器文件
shader = Shader("base.vert", "base.frag")
shader.setAttrib("aPos", 3, GL_FLOAT, 24, 0)
shader.setAttrib("aColor", 3, GL_FLOAT, 24, 12)def render():shader.use()t = (math.sin(time()) + 1) / 2shader.setUniform("t", t)glBindVertexArray(vao)glDrawArrays(GL_TRIANGLES, 0, 3)w.loop(render)

五、结语

本文初步介绍了着色器代码的编写,关于着色器的更多细节,将会在后续章节中逐步介绍。在下一章中,将会介绍纹理的相关内容。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部