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

一、理论基础

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

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

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

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

二、着色器编程

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

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

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

顶点着色器:

#version 330 core

in vec3 aPos;
in vec3 aColor;
out vec3 VertexColor;

void main()
{
    gl_Position = vec4(aPos, 1.0f);
    VertexColor = aColor;
}

片元着色器:

#version 330 core

in 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 布尔型向量
matX X×X的浮点型矩阵
sampler2D 2D纹理
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 含义
f float
i int
ui unsigned int
3f vec3,参数以3个float类型的变量的形式传递
3fv vec3,参数以数组的形式传递

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 time

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

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

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

from time import time

from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.arrays.vbo import VBO
from csdn.window import Window
import numpy as np

w = 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 np

class 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.size
            dtype = value.dtype
            funcs = {
                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)
            return
        elif 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 time

import numpy as np
from OpenGL.GL import *
from OpenGL.arrays.vbo import VBO

# 导入Window类和Shader类
from shader import Shader
from window import Window

w = 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) / 2
    shader.setUniform("t", t)
    glBindVertexArray(vao)
    glDrawArrays(GL_TRIANGLES, 0, 3)


w.loop(render)

五、结语

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

你可能感兴趣的:(PyOpenGL代码实战,着色器)