着色器
安德烈斯 · 科鲁布里
(本教程的代码可在此处获得。)
用 P2D 和 P3D 渲染器在屏幕上绘制的所有处理都是在幕后运行的适当 “默认着色器” 的输出。处理透明地处理这些默认着色器,以便用户无需担心它们,她或他可以继续使用众所周知的绘图功能,并期待与以前版本的处理相同的视觉结果。然而,处理包含了一组新的函数和变量,允许高级用户用她或他自己的替换默认着色器。这开辟了许多令人兴奋的可能性: 使用更复杂的照明和纹理算法渲染 3D 场景,实时应用图像后处理效果,创建非常困难或无法用其他技术生成的复杂程序对象,并在桌面、移动和 web 平台之间共享着色器效果,只需很少的代码更改。
为了理解着色器是如何工作的,以及如何使用它们来扩展处理的绘图功能,有必要概述着色器编程的关键概念,首先是一般的,然后是从加工草图的 “角度”。所以,做好准备,喝一杯你喜欢的饮料,因为这将是一个很长的教程。
回答本节标题中提到的问题,着色器基本上是在计算机的图形处理单元 (GPU) 上运行的程序,并生成我们在屏幕上看到的视觉输出,给定定义 2D 或 3D 场景的信息: 顶点、颜色、纹理、灯光,等等。术语 “着色器” 本身可能有点误导,因为绘图上下文中的阴影一词意味着由于周围的灯光,在物体表面表示不同层次的黑暗以产生深度错觉的过程。第一个计算机着色器主要关注这些着色级别的综合计算,给定三维场景的数学表示及其中对象的材料属性,并尝试创建此类场景的真实感渲染。如今,着色器不仅用于计算虚拟场景中的阴影或照明级别,而且负责所有渲染阶段,从应用于原始几何图形的相机变换开始,到屏幕中每个可见像素的最终颜色评估结束。
有几种语言可以用来编写着色器,如 Cg 、 HLSL 和 GLSL。后者是 OpenGL 中包含的着色器语言,OpenGL 是标准的渲染库和 API,用于各种计算设备,从高端台式计算机到智能手机。GLSL 只是代表 OpenGL 着色语言。由于处理使用 OpenGL 作为其 P2D 和 P3D 渲染器的基础,因此 GLSL 是着色器语言,必须使用它来编写自定义着色器以包含在处理草图中。
编写着色器需要了解使用 GPU 渲染场景所涉及的各个阶段,以及我们如何使用 GLSL 对它们进行编程。这些阶段的顺序在计算机图形学的技术术语中被称为 “图形管道”,现在我们将从加工草图的角度来看管道中的主要阶段。
请注意,本文档的目标不是为 GLSL 提供编程指南,但是要详细描述处理中的新着色器 API,以便已经熟悉 GLSL 的用户可以编写自己的自定义着色器,然后在处理中使用它们。有几种资源,如在线教程和论坛、书籍和网络编码沙箱 (例如着色器玩具、 GLSL 沙箱和顶点着色器艺术),这可以推荐用于学习 GLSL 编程。此外,使用不同的编程接口、平台或工具包 (开放框架、煤渣、 webGL 、 iOS 、安卓等) 获得的 GLSL 经验可以很容易地转化为处理。
让我们从一个简单的 3D 草图作为模型开始,以了解处理函数和变量与在 GPU 上运行的底层管道之间的关系。此草图绘制了一个四边形,其中包含灯光和一些应用于它的几何变换:
代码清单 1.1: 绘制照明旋转矩形的简单 3D 草图
float angle;
void setup() {
size(400, 400, P3D);
noStroke();
}
void draw() {
background(0);
camera(width/2, height/2, 300, width/2, height/2, 0, 0, 1, 0);
pointLight(200, 200, 200, width/2, height/2, -200);
translate(width/2, height/2);
rotateY(angle);
beginShape(QUADS);
normal(0, 0, 1);
fill(50, 50, 200);
vertex(-100, +100);
vertex(+100, +100);
fill(200, 50, 50);
vertex(+100, -100);
vertex(-100, -100);
endShape();
angle += 0.01;
}
下面的图像描绘了图形管道的图表,以及管道的输入如何与草图中的函数调用相关。在一个典型的管道中有几个额外的阶段,但是为了清晰起见,我们没有在这里展示它们。此外,处理遵循 OpenGL ES 设置的规范,OpenGL 是在移动设备中使用的版本,也通过 WebGL 在浏览器中使用。OpenGL ES 中的编程模型更简单,不包括桌面 OpenGL 中存在的所有阶段。在桌面上,OpenGL ES 实际上是 OpenGL 的子集,所以这个选择确保了为处理而编写的 GLSL 着色器可以在不同的平台上使用,并且变化很小。作为一个缺点,OpenGL 桌面的高级功能不能 (直接) 通过处理 API 访问,但是还有其他几个工具包可以用于更复杂的图形编程。
草图中定义的摄影机、灯光、变换和顶点数据为管道提供了两种类型的输入: 统一变量和属性变量。均匀变量是场景中每个顶点保持不变的变量。由于场景中的每个顶点都受到相同投影/模型视图矩阵的影响,因此从摄影机和变换设置中计算出的投影和模型视图矩阵属于此类。出于同样的原因,照明参数 (光源位置、颜色等) 也作为统一变量传递给管道。另一方面,从顶点到顶点变化的变量称为属性,在这个例子中,每个顶点有三种不同类型的属性: xyz 位置本身,使用顶点 () 函数、用 fill() 指定的颜色和法线向量设置。请注意,即使只有一个对 normal() 函数的调用,每个顶点都将有自己的法线向量,在这种情况下,这将是在四个顶点相同。
管道中的第一个阶段是顶点着色器。它使用顶点属性 (在本例中为位置、颜色和法线) 、投影和模型视图矩阵以及灯光参数来计算每个顶点,它在屏幕上的位置和颜色是什么。我们可以想到顶点着色器一次在一个顶点上运行,并执行所有数学运算,以在屏幕平面上投影顶点,并在特定的投影/模型视图矩阵和光源排列的情况下确定其颜色。
顶点着色器不知道顶点是如何相互连接形成形状的,因为它独立于其他顶点接收每个顶点。因此,顶点阶段的直接输出只是投影到屏幕平面上的顶点列表。在处理过程中,我们通过向 beginShape() 传递参数来设置形状的顶点如何相互连接。这个参数,在本例中是四边形,决定了管道中的下一个阶段,称为基元程序集,从顶点着色器中的单个顶点构建几何图元。
确定基元后,下一个阶段包括计算屏幕上哪些像素被绘制基元的面覆盖。但是一个问题是屏幕是像素的离散网格,而直到这一点的几何数据被表示为连续的数值。称为 “光栅化” 的过程负责离散顶点坐标,以便它们可以在给定分辨率下准确地在屏幕上表示。另一个问题是,到目前为止,输出颜色仅在输入顶点计算,需要在基元内部的其余像素处确定。这是通过插值来解决的: 三角形中心的颜色是从三个角顶点的颜色中插值的。有几种方法可以进行这种插值,透视引入的失真也需要考虑。
光栅化和插值阶段的输出是像素位置 (x,y) 及其颜色 (以及可选地可以在着色器中定义的其他变量,我们稍后将讨论这几段)。此信息 (位置、颜色和其他每像素变量) 称为片段。碎片将在下一阶段处理,称为碎片着色器。在这个特殊的例子中,片段着色器没有太大作用; 它只将颜色写入屏幕位置 (x,y)。在这一点上,将片段着色器视为一次对每个沿着管道向下的片段进行操作是很有用的 (就像我们将顶点着色器视为对每个片段进行操作一样)。一次输入一个顶点) 然后将片段的颜色输出到屏幕。为了更清楚地看到这一点,我们可以将顶点着色器想象成一个 “函数”,它在循环中被调用,运行在所有输入顶点上。因此,我们可以用伪代码编写:
for (int i = 0; i < vertexCount; i++) {
output = vertexShader(vertex[i]);
}
//其中顶点着色器函数定义为:
function vertexShader(vertex) {
projPos = projection * modelview * vertex.position;
litColor = lightColor * dot(vertex.normal, lightDirection);
return (projPos, litColor);
}
//同样,我们可以将片段着色器想象成一个 “函数”,它在循环中被调用,运行在所有可见的插值片段上。
//因此,我们可以用伪代码编写:
for (int i = 0; i < fragmentCount; i++) {
screenBuffer[fragment[i].xy] = fragmentShader(fragment[i]);
}
function fragmentShader(fragment) {
return fragment.litColor;
}
请注意,litColor 变量在顶点着色器中计算,然后在片段着色器中访问。这些在顶点和片段着色器之间交换的变量被称为 “变化的”。正如我们之前讨论的,变化变量的值被 GPU 硬件在顶点跨越的片段上插值 (透视校正),所以我们不需要担心这个问题。我们可以定义额外的可变变量,这取决于我们试图用着色器实现的效果类型。
片段着色器可以对每个片段执行额外的操作,实际上这些操作可能非常复杂。完全可以在片段着色器中实现光线跟踪器,我们将通过一些更高级的示例简要介绍这个主题。要记住的片段着色器的一个重要限制是,它不能访问当前正在处理的片段以外的片段 (与顶点着色器不能访问当前正在处理的顶点以外的顶点相同)。操作)。Patricio Gonzalez Vivo 和 Jen Lowe 的着色器之书对 GLSL 着色器进行了出色的图解介绍,对于一般的片段着色器的编程来说,它是一个非常棒的资源。
从我们简化的管道中描述的所有阶段中,只能修改顶点和片段着色器来运行我们自己的自定义代码。其他阶段都是在 GPU 中硬编码的。考虑到这张图片,我们可以继续前进,并开始处理实际的着色器 API。
PShader 类
在上一节中,我们看到 GPU 管道中的两个可编程阶段是顶点和片段着色器 (在 OpenGL 桌面的最新版本中,还有额外的可编程阶段,但是它们在处理过程中没有被着色器 API 覆盖)。为了指定一个完整的工作管道,两者都是必需的。在处理过程中,我们必须在单独的文件中为片段和顶点着色器编写 GLSL 代码,然后将它们组合起来形成一个可以在 GPU 中执行的 “着色器程序”。“程序” 一词经常被省略,隐含的假设是,当我们只是说着色器时,我们指的是一个包含片段和顶点着色器的完整着色器程序。
着色器 (程序) 由 PShader 类封装。使用 loadShader() 函数创建 PShader 对象,该函数将顶点和片段文件的文件名作为参数。如果只指定了一个文件名,则处理将假定文件名对应于片段着色器,并将使用默认的顶点着色器。代码清单 2 显示了草图加载和使用着色器,该着色器使用离散着色级别渲染灯光。
代码清单 2.1: 使用卡通效果着色器渲染球体的草图。
PShader toon;
void setup() {
size(640, 360, P3D);
noStroke();
fill(204);
toon = loadShader("ToonFrag.glsl", "ToonVert.glsl");
toon.set("fraction", 1.0);
}
void draw() {
shader(toon);
background(0);
float dirY = (mouseY / float(height) - 0.5) * 2;
float dirX = (mouseX / float(width) - 0.5) * 2;
directionalLight(204, 204, 204, -dirX, -dirY, -1);
translate(width/2, height/2);
sphere(120);
}
ToonVert.glsl:
uniform mat4 transform;
uniform mat3 normalMatrix;
uniform vec3 lightNormal;
attribute vec4 position;
attribute vec4 color;
attribute vec3 normal;
varying vec4 vertColor;
varying vec3 vertNormal;
varying vec3 vertLightDir;
void main() {
gl_Position = transform * position;
vertColor = color;
vertNormal = normalize(normalMatrix * normal);
vertLightDir = -lightNormal;
}
ToonFrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform float fraction;
varying vec4 vertColor;
varying vec3 vertNormal;
varying vec3 vertLightDir;
void main() {
float intensity;
vec4 color;
intensity = max(0.0, dot(vertLightDir, vertNormal));
if (intensity > pow(0.95, fraction)) {
color = vec4(vec3(1.0), 1.0);
} else if (intensity > pow(0.5, fraction)) {
color = vec4(vec3(0.6), 1.0);
} else if (intensity > pow(0.25, fraction)) {
color = vec4(vec3(0.4), 1.0);
} else {
color = vec4(vec3(0.2), 1.0);
}
gl_FragColor = color * vertColor;
}
片段着色器中的 ifdef 部分是使着色器与 OpenGL ES 和 WebGL 兼容所必需的。它将浮点数和整数的精度设置为中等,这对于大多数设备来说应该没问题。这些 precision 语句在桌面上是可选的。
片段和顶点着色器 (vertColor 、 vertNormal 和 vertLightDir) 之间共享三个可变变量,它们用于按像素而不是按顶点进行照明计算,这是默认值。我们将在本教程的后面看到关于每像素照明的详细示例。顶点着色器中的 gl_Position 变量是 GLSL 内置变量,用于存储输出顶点位置,而 gl_FragColor 是对应的内置变量,用于存储每个片段的颜色输出。Gl_FragColor 的值继续通过 alpha 混合和深度遮挡的 (非可编程) 阶段在管道中进行,以便在屏幕上计算最终颜色。
PShader 类包括 set() 函数,用于将值从草图传递给片段或顶点着色器中的统一变量。如前所述,场景中每个顶点的统一变量保持不变,因此对于整个形状或正在渲染的形状,它们只需要设置一次。一些统一变量是通过处理自动设置的,如变换、正常化矩阵和 lightNormal。下一节详细描述了由处理呈现器自动设置的统一变量和属性变量的名称。
处理中的着色器类型
从前面几节中介绍的代码和伪代码示例中可以看出,不同的着色器具有不同的统一变量和属性变量。例如,不计算照明的着色器不需要固定光源位置和颜色的制服。如果我们使用 C 或 C + + 中的低级工具包,并直接访问 OpenGL API,我们可以自由地以任何我们喜欢的方式命名着色器的制服和属性,因为我们对应用程序中几何图形的存储方式有绝对的控制,以及如何以及何时使用 OpenGL 函数将其传递给 GPU 管道。这在处理过程中是不同的,因为着色器是由渲染器自动处理的,并且应该能够处理用处理的绘图 API 描述的几何图形。这并不意味着自定义着色器必须以与默认情况下处理相同的方式呈现事物,恰恰相反,自定义着色器的使用打开了在处理中大大改变渲染管道的可能性。然而,与标准绘图 API 结合使用的自定义着色器必须遵循某些命名约定,并且受一些限制。
根据场景是否有笔画 (线或点),或者它使用纹理和/或灯光,然后,在这些情况下使用的着色器必须呈现一组特定的统一变量和属性变量,这些变量将允许处理与着色器接口并向其发送适当的数据。
基于这种统一和属性要求,处理中的着色器必须属于 6 种不同类型之一。这 6 种类型可以分为 3 类:
- 点着色器: 用于渲染描边点。
- 线着色器: 用于渲染描边线。
- 三角形着色器: 用于渲染其他任何东西,这意味着它们将处理 (光照/未光照、纹理/非纹理) 形状。因为处理中的所有形状最终都是由三角形制成的,所以它们可以被称为三角形着色器。
使用着色器 () 函数设置着色器时,我们可以显式指定着色器类型:
void draw() {
shader(pointShader, POINTS);
shader(lineShader, LINES);
shader(polyShader); //处理将自动检测
stroke(255);
beginShape(POLYGON);
...
}
此代码将导致使用 pointShader 来渲染笔划点,使用 lineShader 来渲染线条,使用 polyShader 来渲染适当类型的几何图形。
仅当使用 P3D 渲染器时,点和线着色器才相关。三维中的点和线的渲染与常规几何图形的渲染非常不同,因为它们需要始终面向屏幕。这需要顶点着色器中的不同变换数学。相比之下,二维的所有几何图形,包括直线和点,都被渲染为规则三角形,因为它包含在与屏幕平行的单个平面中。
然而,由于灯光和纹理的使用,三角形着色器有不同的类型。渲染规则的非笔触几何图形时,实际上有 4 种不同的情况:
- 没有灯光,也没有纹理
- 有灯光但没有纹理
- 有纹理,但没有灯光
- 有纹理和灯光
如前所述,渲染照明几何的着色器需要从处理中发送额外的属性和统一变量,而着色器不需要这些属性和变量,着色器只渲染没有灯光或纹理的平面彩色几何。同样,用于渲染纹理多边形的着色器需要其自己的制服和属性 (纹理采样器和纹理坐标),否则将不需要。根据着色器中定义的制服和属性,它将能够正确渲染当前场景。
因此,三角形着色器被进一步区分为 4 种类型:
- 颜色: 渲染没有灯光和纹理的几何图形
- 灯光: 用灯光渲染几何图形,但没有纹理
- 纹理: 用纹理渲染几何,但没有灯光
- TEXLIGHT: 使用纹理和灯光渲染几何图形
与前面提到的点和线着色器一起,我们最终有 6 种不同类型的着色器在处理中。在下一节中,我们将详细描述每种类型的着色器。
着色器类型可以使用预处理器 # 定义在着色器的源代码中明确设置,这覆盖了处理的自动检测。支持的 # 定义如下:
- #define PROCESSING_POINT_SHADER
- #define PROCESSING_LINE_SHADER
- #define PROCESSING_COLOR_SHADER
- #define PROCESSING_LIGHT_SHADER
- #define PROCESSING_TEXTURE_SHADER
- #define PROCESSING_TEXLIGHT_SHADER
颜色着色器
我们将浏览处理中可用的不同类型的着色器,并检查每个着色器的示例。对于颜色、光线、纹理和 texlight 着色器,我们将在所有示例中使用相同的基础几何图形,即圆柱体。下一个代码将作为所有后续草图的基础:
代码清单 4.1: 基本气缸。
PShape can;
float angle;
PShader colorShader;
void setup() {
size(640, 360, P3D);
can = createCan(100, 200, 32);
}
void draw() {
background(0);
translate(width/2, height/2);
rotateY(angle);
shape(can);
angle += 0.01;
}
PShape createCan(float r, float h, int detail) {
textureMode(NORMAL);
PShape sh = createShape();
sh.beginShape(QUAD_STRIP);
sh.noStroke();
for (int i = 0; i <= detail; i++) {
float angle = TWO_PI / detail;
float x = sin(i * angle);
float z = cos(i * angle);
float u = float(i) / detail;
sh.normal(x, 0, z);
sh.vertex(x * r, -h/2, z * r, u, 0);
sh.vertex(x * r, +h/2, z * r, u, 1);
}
sh.endShape();
return sh;
}
输出应该是一个围绕 y轴旋转的纯白色圆柱体。因此,让我们编写第一个着色器,以更有趣的方式渲染这个圆柱体!
首先,保存清单 1 中的代码,并在数据文件夹中添加以下两个文件:
代码清单 4.2: 用于颜色渲染的顶点和片段着色器。
colorvert.glsl:
uniform mat4 transform;
attribute vec4 position;
attribute vec4 color;
varying vec4 vertColor;
void main() {
gl_Position = transform * position;
vertColor = color;
}
colorfrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
varying vec4 vertColor;
void main() {
gl_FragColor = vertColor;
}
现在,我们需要对主程序进行两次更改,以使用我们的新着色器。在我们的设置功能结束时,我们需要添加
colorShader = loadShader("colorfrag.glsl", "colorvert.glsl");
加载着色器文件。然后,在背景 () 之后,添加
shader(colorShader);
使用我们的着色器绘制罐头。
着色器的类型在这里用顶点着色器中的定义表示,但也可以在片段着色器中。在本例中,只有一个统一变量 transform,它是一个 4x4 矩阵,该矩阵包含投影和 modelview 矩阵的乘积。通过变换矩阵将世界坐标中的位置相乘得到剪辑坐标。位置和颜色属性分别保存输入顶点的位置和颜色。输入颜色被复制,而不会对 vertColor 变化进行任何修改,这会将值传递给片段着色器。片段着色器只不过是一个传递着色器,因为颜色被发送到输出,而没有任何进一步的修改。通过处理自动设置均匀变换和属性位置和颜色。
我们可以通过在片段着色器中进行以下更改来进行一些简单的颜色操作:
Gl_FragColor = vec4(vec3(1)-vertColor.xyz,1);
这将在屏幕上显示输入颜色的倒置。因此,例如,如果我们在 sh.beginShape(QUAD_STRIP) 之后添加 sh.fill(255,255,0),那么圆柱体应该被涂成蓝色。
纹理着色器
渲染纹理几何体需要在着色器中添加其他制服和属性。让我们一起看看下面的草图以及附带的片段和顶点着色器:
代码清单 5.1: 纹理渲染的草图 (无灯光)。
PImage label;
PShape can;
float angle;
PShader texShader;
void setup() {
size(640, 360, P3D);
label = loadImage("lachoy.jpg");
can = createCan(100, 200, 32, label);
texShader = loadShader("texfrag.glsl", "texvert.glsl");
}
void draw() {
background(0);
shader(texShader);
translate(width/2, height/2);
rotateY(angle);
shape(can);
angle += 0.01;
}
PShape createCan(float r, float h, int detail, PImage tex) {
textureMode(NORMAL);
PShape sh = createShape();
sh.beginShape(QUAD_STRIP);
sh.noStroke();
sh.texture(tex);
for (int i = 0; i <= detail; i++) {
float angle = TWO_PI / detail;
float x = sin(i * angle);
float z = cos(i * angle);
float u = float(i) / detail;
sh.normal(x, 0, z);
sh.vertex(x * r, -h/2, z * r, u, 0);
sh.vertex(x * r, +h/2, z * r, u, 1);
}
sh.endShape();
return sh;
}
texvert.glsl:
uniform mat4 transform;
uniform mat4 texMatrix;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
varying vec4 vertColor;
varying vec4 vertTexCoord;
void main() {
gl_Position = transform * position;
vertColor = color;
vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0);
}
texfrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform sampler2D texture;
varying vec4 vertColor;
varying vec4 vertTexCoord;
void main() {
gl_FragColor = texture2D(texture, vertTexCoord.st) * vertColor;
}
顶点着色器中有一个名为 texMatrix 的新统一,它为每个顶点重新调整纹理坐标 (在附加属性 texCoord 中传递),考虑到沿 y轴的纹理反转 (因为处理的垂直轴相对于 OpenGL 的是反转的),以及两个纹理的非幂(NPOT 纹理的纹理坐标值为 1.0 将重新调整为覆盖 NPOT 范围的较小值)。在片段着色器中,我们有一个新的 sampler2D 类型的统一变量,纹理,它基本上表示指向纹理数据的指针,增加了在任意纹理坐标下采样的容量 (i。e: 不一定在 texel 的中心)。根据纹理的采样配置 (线性、双线性等),GPU 将使用不同的插值算法。该草图的结果在下图中显示:
在片段着色器级别实现像素化效果变得非常容易。我们所需要做的就是修改纹理坐标值 vertTexCoord.st,以便它们在给定数量的单元格中被绑定,在本例中为 50:
void main() {
int si = int(vertTexCoord.s * 50.0);
int sj = int(vertTexCoord.t * 50.0);
gl_FragColor = texture2D(texture, vec2(float(si) / 50.0, float(sj) / 50.0))
* vertColor;
}
关于纹理着色器的最后一点是它们也用于渲染文本。这样做的原因是,P2D/P3D 渲染器在处理中将文本绘制为纹理四边形,由纹理着色器处理。
灯光着色器
照明 3D 场景包括在空间中放置一个或多个光源,并定义其参数,例如类型 (点、聚光灯) 和颜色 (漫射、环境、镜面)。我们不会讨论 OpenGL 中照明的细节,但是这里要提到的重要一点是,我们用来用 GLSL 着色器生成灯光的所有数学模型都是对现实世界中灯光的非常简单的近似。我们将在以下示例中使用的模型可能是最简单的模型之一,并评估对象每个顶点的光照强度,然后使用图形管道的内置插值来获得对象表面的连续颜色渐变。每个顶点的光强度计算为顶点法线与顶点和光位置之间的方向向量之间的点积。该模型表示一个点光源,该点光源在所有方向上平均发光:
使用前面示例中的相同几何图形,我们现在可以编写一个简单的着色器,以使用单点灯光渲染场景。为了做到这一点,我们需要在顶点着色器中增加一些额外的均匀变量: 包含光源位置的光照位置,以及归一化矩阵,这是一个 3x3 矩阵,用于将法线向量转换为适当的坐标来执行照明计算。
代码清单 6.1: 简单 (每个顶点) 照明的草图。由于 createCan() 函数与清单 3 相同,因此省略了草图代码中 createCan() 函数的实现。
PShape can;
float angle;
PShader lightShader;
void setup() {
size(640, 360, P3D);
can = createCan(100, 200, 32);
lightShader = loadShader("lightfrag.glsl", "lightvert.glsl");
}
void draw() {
background(0);
shader(lightShader);
pointLight(255, 255, 255, width/2, height, 200);
translate(width/2, height/2);
rotateY(angle);
shape(can);
angle += 0.01;
}
lightvert.glsl:
uniform mat4 modelview;
uniform mat4 transform;
uniform mat3 normalMatrix;
uniform vec4 lightPosition;
attribute vec4 position;
attribute vec4 color;
attribute vec3 normal;
varying vec4 vertColor;
void main() {
gl_Position = transform * position;
vec3 ecPosition = vec3(modelview * position);
vec3 ecNormal = normalize(normalMatrix * normal);
vec3 direction = normalize(lightPosition.xyz - ecPosition);
float intensity = max(0.0, dot(direction, ecNormal));
vertColor = vec4(intensity, intensity, intensity, 1) * color;
}
lightfrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
varying vec4 vertColor;
void main() {
gl_FragColor = vertColor;
}
在顶点着色器中,ecPosition 变量是用眼睛坐标表示的输入顶点的位置,因为它是通过顶点乘以 modelview 矩阵获得的。类似地,将输入法线向量乘以法线矩阵会在眼睛系统中产生它的坐标。一旦所有矢量都在同一坐标系中表示,它们就可以用来计算当前矢量上的入射光强度。从着色器中使用的公式来看,强度与顶点和光源之间的法线和矢量之间的角度成正比。
在本例中,有一个单点光源,但是处理最多可以向着色器发送 8 个不同的光源及其相关参数。可用于在着色器中获取此信息的灯光制服的完整列表如下:
- uniform int lightCount: 活动灯光的数量
- uniform vec4 lightPosition[8]: 每个灯的位置
- uniform vec3 lightNormal[8]: 每盏灯的方向 (仅与定向灯和聚光灯相关)
- uniform vec3 lightAmbient[8]: 光色的环境分量
- uniform vec3 lightDiffuse[8]: 浅色的漫射分量
- uniform vec3 lightSpecular[8]: 浅色镜面成分
- uniform vec3 lightFalloff[8]: 光衰减系数
- uniform vec2 lightSpot[8]: l光斑参数 (光斑角度和浓度的余弦)
此制服中的值完全指定在处理过程中使用 ambientLight() 、 pointLight() 、 directionalLight() 和 spotLight() 函数在草图中设置的任何照明配置。要了解所有这些制服是如何在默认灯光着色器中使用的,该着色器涵盖了所有可能的灯光组合,请从处理核心查看其源代码。然而,有效的光着色器不需要声明所有这些制服,例如在前面的例子中,我们只需要光位置制服。
正如在开始时讨论的那样,设置自定义着色器的可能性允许我们为更复杂或生成特定视觉样式的着色器更改默认渲染算法。例如,我们可以通过更精确的每像素照明计算来替换上述基于顶点的模型,从而使灯光看起来更好。这个想法是插值法线和方向向量,而不是顶点的最终颜色,然后通过使用从带有可变变量的顶点着色器传递的法线和方向来计算每个片段的强度值。
代码清单 6.2: 简单每像素照明的顶点和片段着色器。
pixlightvert.glsl:
uniform mat4 modelview;
uniform mat4 transform;
uniform mat3 normalMatrix;
uniform vec4 lightPosition;
uniform vec3 lightNormal;
attribute vec4 position;
attribute vec4 color;
attribute vec3 normal;
varying vec4 vertColor;
varying vec3 ecNormal;
varying vec3 lightDir;
void main() {
gl_Position = transform * position;
vec3 ecPosition = vec3(modelview * position);
ecNormal = normalize(normalMatrix * normal);
lightDir = normalize(lightPosition.xyz - ecPosition);
vertColor = color;
}
pixlightfrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
varying vec4 vertColor;
varying vec3 ecNormal;
varying vec3 lightDir;
void main() {
vec3 direction = normalize(lightDir);
vec3 normal = normalize(ecNormal);
float intensity = max(0.0, dot(direction, normal));
gl_FragColor = vec4(intensity, intensity, intensity, 1) * vertColor;
}
每个顶点和每个像素照明算法的输出可以在下两个图中进行比较,其中第一个对应于每个顶点照明,第二个对应于每个像素。对象每个面顶点上的颜色值的 (线性) 插值会导致面边缘上的光照水平发生明显变化。通过内插向量,变化更加平滑。
纹理光着色器
三角形着色器的最终类型是纹理光 (texlight) 着色器。这种类型的着色器结合了灯光和纹理类型的制服。
我们可以在下面的草图中整合前几节中的示例:
代码清单 7.1: 结合纹理和顶点照明的草图。由于 createCan() 函数与清单 5.1 相同,因此省略了草图代码中 createCan() 函数的实现。
PImage label;
PShape can;
float angle;
PShader texlightShader;
void setup() {
size(640, 360, P3D);
label = loadImage("lachoy.jpg");
can = createCan(100, 200, 32, label);
texlightShader = loadShader("texlightfrag.glsl", "texlightvert.glsl");
}
void draw() {
background(0);
shader(texlightShader);
pointLight(255, 255, 255, width/2, height, 200);
translate(width/2, height/2);
rotateY(angle);
shape(can);
angle += 0.01;
}
texlightvert.glsl:
uniform mat4 modelview;
uniform mat4 transform;
uniform mat3 normalMatrix;
uniform mat4 texMatrix;
uniform vec4 lightPosition;
attribute vec4 position;
attribute vec4 color;
attribute vec3 normal;
attribute vec2 texCoord;
varying vec4 vertColor;
varying vec4 vertTexCoord;
void main() {
gl_Position = transform * position;
vec3 ecPosition = vec3(modelview * position);
vec3 ecNormal = normalize(normalMatrix * normal);
vec3 direction = normalize(lightPosition.xyz - ecPosition);
float intensity = max(0.0, dot(direction, ecNormal));
vertColor = vec4(intensity, intensity, intensity, 1) * color;
vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0);
}
texlightfrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform sampler2D texture;
varying vec4 vertColor;
varying vec4 vertTexCoord;
void main() {
gl_FragColor = texture2D(texture, vertTexCoord.st) * vertColor;
}
照明也可以在每像素的基础上完成,在这种情况下,我们需要使用以下着色器:
代码清单 7.2: 纹理和每像素照明的着色器。
pixlightxvert.glsl:
uniform mat4 modelview;
uniform mat4 transform;
uniform mat3 normalMatrix;
uniform mat4 texMatrix;
uniform vec4 lightPosition;
attribute vec4 position;
attribute vec4 color;
attribute vec3 normal;
attribute vec2 texCoord;
varying vec4 vertColor;
varying vec3 ecNormal;
varying vec3 lightDir;
varying vec4 vertTexCoord;
void main() {
gl_Position = transform * position;
vec3 ecPosition = vec3(modelview * position);
ecNormal = normalize(normalMatrix * normal);
lightDir = normalize(lightPosition.xyz - ecPosition);
vertColor = color;
vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0);
}
pixlightxfrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform sampler2D texture;
varying vec4 vertColor;
varying vec3 ecNormal;
varying vec3 lightDir;
varying vec4 vertTexCoord;
void main() {
vec3 direction = normalize(lightDir);
vec3 normal = normalize(ecNormal);
float intensity = max(0.0, dot(direction, normal));
vec4 tintColor = vec4(intensity, intensity, intensity, 1) * vertColor;
gl_FragColor = texture2D(texture, vertTexCoord.st) * tintColor;
}
请注意,texlight 不能用于仅使用纹理或仅使用灯光渲染场景,在这些情况下,将需要纹理或灯光着色器。
制服和属性的别名
一些最常见的顶点属性和着色器制服有一个替代的简短名称,如下所示:
- position or vertex
- transform or transformMatrix
- modelview or modelviewMatrix
- projection or projectionMatrix
- texture or texMap
图像后处理效果
通过利用 gpu 的并行特性,片段着色器可以非常有效地运行图像后处理效果。例如,让我们想象一下,我们希望仅使用黑白渲染纹理: 如果图像中给定像素的亮度低于某个阈值,则为黑色,如果上面是白色的。这可以通过以下代码在片段着色器中轻松实现:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform sampler2D texture;
varying vec4 vertColor;
varying vec4 vertTexCoord;
const vec4 lumcoeff = vec4(0.299, 0.587, 0.114, 0);
void main() {
vec4 col = texture2D(texture, vertTexCoord.st);
float lum = dot(col, lumcoeff);
if (0.5 < lum) {
gl_FragColor = vertColor;
} else {
gl_FragColor = vec4(0, 0, 0, 1);
}
}
片段着色器在 vertTexCoord.st 位置对纹理进行采样,并使用颜色值计算亮度,然后根据阈值计算两个替代输出,在本例中为 0.5。我们可以将此着色器与前面示例中的纹理对象一起使用:
代码清单 8.1: 使用黑白着色器绘制草图。
PImage label;
PShape can;
float angle;
PShader bwShader;
void setup() {
size(640, 360, P3D);
label = loadImage("lachoy.jpg");
can = createCan(100, 200, 32, label);
bwShader = loadShader("bwfrag.glsl");
}
void draw() {
background(0);
shader(bwShader);
translate(width/2, height/2);
rotateY(angle);
shape(can);
angle += 0.01;
}
您会注意到,这一次 loadShader() 函数只接收片段着色器的文件名。处理如何完成整个着色器程序?答案是它为纹理着色器使用默认的顶点阶段。因此,由于可变变量首先在顶点阶段声明,片段着色器必须遵循默认着色器中采用的可变名称。在这种情况下,片段颜色和纹理坐标的变化变量必须分别命名为 vertColor 和 vertTexCoord。
卷积过滤器也可以在片段着色器中实现。给定片段 vertTexCoord 的纹理坐标,纹理中的相邻像素 (也称为 “texels”) 可以使用 texOffset 制服进行采样。该统一是通过处理自动设置的,包含矢量 (1/宽度,1/高度),宽度和高度是纹理的分辨率。这些值正是沿水平和垂直方向的偏移,需要从 vertTexCoord.st 周围的纹理中取样颜色。例如,vertTexCoord.st + vec2(texOffset.s,0) 正好是右侧的一个位置。以下 GLSL 代码显示了标准边缘检测和浮雕过滤器的实现:
代码清单 8.2: 边缘检测着色器。
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform sampler2D texture;
uniform vec2 texOffset;
varying vec4 vertColor;
varying vec4 vertTexCoord;
const vec4 lumcoeff = vec4(0.299, 0.587, 0.114, 0);
void main() {
vec2 tc0 = vertTexCoord.st + vec2(-texOffset.s, -texOffset.t);
vec2 tc1 = vertTexCoord.st + vec2( 0.0, -texOffset.t);
vec2 tc2 = vertTexCoord.st + vec2(+texOffset.s, -texOffset.t);
vec2 tc3 = vertTexCoord.st + vec2(-texOffset.s, 0.0);
vec2 tc4 = vertTexCoord.st + vec2( 0.0, 0.0);
vec2 tc5 = vertTexCoord.st + vec2(+texOffset.s, 0.0);
vec2 tc6 = vertTexCoord.st + vec2(-texOffset.s, +texOffset.t);
vec2 tc7 = vertTexCoord.st + vec2( 0.0, +texOffset.t);
vec2 tc8 = vertTexCoord.st + vec2(+texOffset.s, +texOffset.t);
vec4 col0 = texture2D(texture, tc0);
vec4 col1 = texture2D(texture, tc1);
vec4 col2 = texture2D(texture, tc2);
vec4 col3 = texture2D(texture, tc3);
vec4 col4 = texture2D(texture, tc4);
vec4 col5 = texture2D(texture, tc5);
vec4 col6 = texture2D(texture, tc6);
vec4 col7 = texture2D(texture, tc7);
vec4 col8 = texture2D(texture, tc8);
vec4 sum = 8.0 * col4 - (col0 + col1 + col2 + col3 + col5 + col6 + col7 + col8);
gl_FragColor = vec4(sum.rgb, 1.0) * vertColor;
}
代码清单 8.3: 浮雕着色器。
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform sampler2D texture;
uniform vec2 texOffset;
varying vec4 vertColor;
varying vec4 vertTexCoord;
const vec4 lumcoeff = vec4(0.299, 0.587, 0.114, 0);
void main() {
vec2 tc0 = vertTexCoord.st + vec2(-texOffset.s, -texOffset.t);
vec2 tc1 = vertTexCoord.st + vec2( 0.0, -texOffset.t);
vec2 tc2 = vertTexCoord.st + vec2(-texOffset.s, 0.0);
vec2 tc3 = vertTexCoord.st + vec2(+texOffset.s, 0.0);
vec2 tc4 = vertTexCoord.st + vec2( 0.0, +texOffset.t);
vec2 tc5 = vertTexCoord.st + vec2(+texOffset.s, +texOffset.t);
vec4 col0 = texture2D(texture, tc0);
vec4 col1 = texture2D(texture, tc1);
vec4 col2 = texture2D(texture, tc2);
vec4 col3 = texture2D(texture, tc3);
vec4 col4 = texture2D(texture, tc4);
vec4 col5 = texture2D(texture, tc5);
vec4 sum = vec4(0.5) + (col0 + col1 + col2) - (col3 + col4 + col5);
float lum = dot(sum, lumcoeff);
gl_FragColor = vec4(lum, lum, lum, 1.0) * vertColor;
}
这些后处理效果的输出显示在以下图像中 (b & w 、从上到下的边缘和浮雕):
把所有的东西放在一起
在到目前为止我们看到的所有示例中,在整个草图的执行过程中只使用一个着色器。但是,我们可以根据需要加载和使用任意数量的着色器。在每个给定的时间,一个着色器将被选择并运行,但是我们可以使用着色器 () 函数更改选定的着色器,并使用 resetShader() 恢复默认着色器。以下示例结合了前面部分中的所有着色器,因此可以使用键盘启用/禁用它们:
代码清单 9.1: 到目前为止我们看到的所有着色器。
float canSize = 60;
PImage label;
PShape can;
PShape cap;
float angle;
PShader selShader;
boolean useLight;
boolean useTexture;
PShader colorShader;
PShader lightShader;
PShader texShader;
PShader texlightShader;
PShader pixlightShader;
PShader texlightxShader;
PShader bwShader;
PShader edgesShader;
PShader embossShader;
void setup() {
size(480, 480, P3D);
label = loadImage("lachoy.jpg");
can = createCan(canSize, 2 * canSize, 32, label);
cap = createCap(canSize, 32);
colorShader = loadShader("colorfrag.glsl", "colorvert.glsl");
lightShader = loadShader("lightfrag.glsl", "lightvert.glsl");
texShader = loadShader("texfrag.glsl", "texvert.glsl");
texlightShader = loadShader("texlightfrag.glsl", "texlightvert.glsl");
pixlightShader = loadShader("pixlightfrag.glsl", "pixlightvert.glsl");
texlightxShader = loadShader("pixlightxfrag.glsl", "pixlightxvert.glsl");
bwShader = loadShader("bwfrag.glsl");
edgesShader = loadShader("edgesfrag.glsl");
embossShader = loadShader("embossfrag.glsl");
selShader = texlightShader;
useLight = true;
useTexture = true;
println("Vertex lights, texture shading");
}
void draw() {
background(0);
float x = 1.88 * canSize;
float y = 2 * canSize;
int n = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
drawCan(x, y, angle);
x += 2 * canSize + 8;
n++;
}
x = 1.88 * canSize;
y += 2 * canSize + 5;
}
angle += 0.01;
}
void drawCan( float centerx, float centery, float rotAngle) {
pushMatrix();
if (useLight) {
pointLight(255, 255, 255, centerx, centery, 200);
}
shader(selShader);
translate(centerx, centery, 65);
rotateY(rotAngle);
if (useTexture) {
can.setTexture(label);
} else {
can.setTexture(null);
}
shape(can);
noLights();
resetShader();
pushMatrix();
translate(0, canSize - 5, 0);
shape(cap);
popMatrix();
pushMatrix();
translate(0, -canSize + 5, 0);
shape(cap);
popMatrix();
popMatrix();
}
PShape createCan(float r, float h, int detail, PImage tex) {
textureMode(NORMAL);
PShape sh = createShape();
sh.beginShape(QUAD_STRIP);
sh.noStroke();
sh.texture(tex);
for (int i = 0; i <= detail; i++) {
float angle = TWO_PI / detail;
float x = sin(i * angle);
float z = cos(i * angle);
float u = float(i) / detail;
sh.normal(x, 0, z);
sh.vertex(x * r, -h/2, z * r, u, 0);
sh.vertex(x * r, +h/2, z * r, u, 1);
}
sh.endShape();
return sh;
}
PShape createCap(float r, int detail) {
PShape sh = createShape();
sh.beginShape(TRIANGLE_FAN);
sh.noStroke();
sh.fill(128);
sh.vertex(0, 0, 0);
for (int i = 0; i <= detail; i++) {
float angle = TWO_PI / detail;
float x = sin(i * angle);
float z = cos(i * angle);
sh.vertex(x * r, 0, z * r);
}
sh.endShape();
return sh;
}
void keyPressed() {
if (key == '1') {
println("No lights, no texture shading");
selShader = colorShader;
useLight = false;
useTexture = false;
} else if (key == '2') {
println("Vertex lights, no texture shading");
selShader = lightShader;
useLight = true;
useTexture = false;
} else if (key == '3') {
println("No lights, texture shading");
selShader = texShader;
useLight = false;
useTexture = true;
} else if (key == '4') {
println("Vertex lights, texture shading");
selShader = texlightShader;
useLight = true;
useTexture = true;
} else if (key == '5') {
println("Pixel lights, no texture shading");
selShader = pixlightShader;
useLight = true;
useTexture = false;
} else if (key == '6') {
println("Pixel lights, texture shading");
selShader = texlightxShader;
useLight = true;
useTexture = true;
} else if (key == '7') {
println("Black&white texture filtering");
selShader = bwShader;
useLight = false;
useTexture = true;
} else if (key == '8') {
println("Edge detection filtering");
selShader = edgesShader;
useLight = false;
useTexture = true;
} else if (key == '9') {
println("Emboss filtering");
selShader = embossShader;
useLight = false;
useTexture = true;
}
}
及其可能的输出之一:
请注意,can 圆柱体的瓶盖不受任何自定义着色器的影响,因为它们是在重置着色器 () 调用后绘制的。Cap 模型没有纹理,灯光也被禁用,这意味着处理将使用其默认颜色着色器来渲染 cap。
10.点和线着色器
P3D 渲染器仅使用点和线着色器来绘制笔划几何图形。原因是笔画几何图形在 3D 中始终是面向屏幕的,因此它需要在顶点阶段进行不同的投影计算。在 P2D 中,所有几何图形都包含在同一平面中,因此不需要对笔划线和点进行特殊处理 (所有内容都只是用正投影渲染为三角形)。
渲染面向屏幕的多边形的技术被称为 “广告牌”,并已广泛用于游戏中,通过使用始终面对相机的 2D 图像 (图像从广告牌灯塔 3d 教程):
在 GLSL 代码方面,(面向屏幕) 广告牌的基本思想可以在下面一行中看到:
gl_Position = transform * center + projection * vec4(offset, 0, 0);
变量中心表示广告牌的中心位置,偏移是从中心点到边缘顶点的 (2D) 位移。因为偏移没有与 modelview 矩阵 (几何变换和相机放置) 变换,所以由于投影变换,它会导致沿屏幕平面应用的平移。
让我们从点渲染开始。P3D 将点表示为矩形 (当笔划帽设置为正方形时) 或 n 边的规则多边形 (笔划帽设置为圆形)。根据点的大小动态调整边的数量,以确保看起来与真实的圆足够相似。以下草图演示了一个简单的点渲染着色器:
代码清单 10.1: 带有关联着色器的简单点渲染草图。
PShader pointShader;
void setup() {
size(640, 360, P3D);
pointShader = loadShader("pointfrag.glsl", "pointvert.glsl");
stroke(255);
strokeWeight(50);
background(0);
}
void draw() {
shader(pointShader, POINTS);
if (mousePressed) {
point(mouseX, mouseY);
}
}
pointvert.glsl:
uniform mat4 projection;
uniform mat4 modelview;
attribute vec4 position;
attribute vec4 color;
attribute vec2 offset;
varying vec4 vertColor;
void main() {
vec4 pos = modelview * position;
vec4 clip = projection * pos;
gl_Position = clip + projection * vec4(offset, 0, 0);
vertColor = color;
}
pointfrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
varying vec4 vertColor;
void main() {
gl_FragColor = vertColor;
}
属性偏移包含需要从点的中心应用的位移,以生成所有边界顶点。对于正方形点,P3D 渲染器向顶点着色器发送 5 个顶点,所有顶点都与点的中心重合。第一个顶点在其偏移属性中具有 (0,0),因此它保留在点的中心,而其他顶点有适当的偏移来将它们移动到正方形的角落,如下图所示:
默认的点着色器不允许对点应用任何纹理,但是可以通过自定义着色器来规避此限制,该着色器具有纹理采样器的附加统一。让我们在下一个示例中看看如何使用此技巧创建渲染纹理点精灵的着色器。
代码清单 10.2: 使用自定义点着色器渲染精灵的草图。
PShader pointShader;
PImage cloud1;
PImage cloud2;
PImage cloud3;
float weight = 100;
void setup() {
size(640, 360, P3D);
pointShader = loadShader("spritefrag.glsl", "spritevert.glsl");
pointShader.set("weight", weight);
cloud1 = loadImage("cloud1.png");
cloud2 = loadImage("cloud2.png");
cloud3 = loadImage("cloud3.png");
pointShader.set("sprite", cloud1);
strokeWeight(weight);
strokeCap(SQUARE);
stroke(255, 70);
background(0);
}
void draw() {
shader(pointShader, POINTS);
if (mousePressed) {
point(mouseX, mouseY);
}
}
void mousePressed() {
if (key == '1') {
pointShader.set("sprite", cloud1);
} else if (key == '2') {
pointShader.set("sprite", cloud2);
} else if (key == '3') {
pointShader.set("sprite", cloud3);
}
}
spritevert.glsl:
uniform mat4 projection;
uniform mat4 modelview;
uniform float weight;
attribute vec4 position;
attribute vec4 color;
attribute vec2 offset;
varying vec4 vertColor;
varying vec2 texCoord;
void main() {
vec4 pos = modelview * position;
vec4 clip = projection * pos;
gl_Position = clip + projection * vec4(offset, 0, 0);
texCoord = (vec2(0.5) + offset / weight);
vertColor = color;
}
spritefrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform sampler2D sprite;
varying vec4 vertColor;
varying vec2 texCoord;
void main() {
gl_FragColor = texture2D(sprite, texCoord) * vertColor;
}
在本例中,我们使用 PShader.set() 函数从草图中显式设置纹理采样器,该函数只是将封装纹理的 PImage 对象作为参数。在片段着色器中使用此采样器与我们之前在纹理着色器中看到的相同。但是,纹理采样需要纹理坐标,并且处理不会将任何点发送到着色器,因为默认点从不进行纹理处理。因此,我们需要在顶点着色器中手动计算纹理坐标。给定偏移属性中包含的位移值,可以通过注意偏移范围从-weight/2 到 + weight/2 沿每个方向轻松计算每个角点的纹理坐标。所以等式:
texCoord = (vec2(0.5) + offset / weight);
将偏移范围映射到纹理化所需的 (0,1) 间隔。变量 texCoord 是变化的,这意味着它将被插值到正方形中的所有片段上。此外,我们通过另一个自定义制服将笔划权重直接发送到着色器。这有点多余,因为偏移量已经包含了权重值 (因为它是位移的大小),但是为了这个例子的目的,我们使用权重均匀而不是从偏移量中提取,以保持着色器代码更简单。
处理中默认点渲染的另一个限制是,我们可以通过自定义点着色器来克服,它使用许多顶点来表示单个点。我们看到,在正方形点的情况下,每个点实际上有 5 个顶点被发送到 GPU。在轮点的情况下,这个数字可能会明显更高,因为点圆是用 n 边的规则多边形近似的。数字 n 随着屏幕上的点变大而增长。
另一种解决方案是在片段着色器中按程序生成点。让我们看看如何在下面的例子中做到这一点: 点仍然作为 5 个顶点的正方形发送到 GPU,但是片段着色器计算哪些像素在距离正方形中心的笔划权重距离内,使其余像素透明,因此,结果由高质量的圆点组成。
代码清单 10.3: 绘制程序生成的圆点的草图。
PShader pointShader;
void setup() {
size(640, 360, P3D);
pointShader = loadShader("pointfrag.glsl", "pointvert.glsl");
pointShader.set("sharpness", 0.9);
strokeCap(SQUARE);
background(0);
}
void draw() {
if (mousePressed) {
shader(pointShader, POINTS);
float w = random(5, 50);
pointShader.set("weight", w);
strokeWeight(w);
stroke(random(255), random(255), random(255));
point(mouseX, mouseY);
}
}
pointvert.glsl:
uniform mat4 projection;
uniform mat4 transform;
attribute vec4 position;
attribute vec4 color;
attribute vec2 offset;
varying vec4 vertColor;
varying vec2 center;
varying vec2 pos;
void main() {
vec4 clip = transform * position;
gl_Position = clip + projection * vec4(offset, 0, 0);
vertColor = color;
center = clip.xy;
pos = offset;
}
pointfrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform float weight;
uniform float sharpness;
varying vec4 vertColor;
varying vec2 center;
varying vec2 pos;
void main() {
float len = weight/2.0 - length(pos);
vec4 color = vec4(1.0, 1.0, 1.0, len);
color = mix(vec4(0.0), color, sharpness);
color = clamp(color, 0.0, 1.0);
gl_FragColor = color * vertColor;
}
我们现在将看看线着色器。P3D 渲染器将线绘制为四边形序列,但这些四边形需要面向屏幕。用于制作四边形屏幕的方法类似于用于点的方法: 每个直线顶点都有一个名为 “方向” 的关联属性变量,该变量包含将当前点连接到四边形的相反点的向量,以及直线的厚度,根据行程重量计算。切线向量只是标准化的方向向量,它用于计算沿法线方向的偏移:
以下草图仅使用默认线着色器的简化版本绘制框的边:
代码清单 10.4: 简单线条着色器。
PShape cube;
PShader lineShader;
float angle;
float weight = 10;
void setup() {
size(640, 360, P3D);
lineShader = loadShader("linefrag.glsl", "linevert.glsl");
cube = createShape(BOX, 150);
cube.setFill(false);
cube.setStroke(color(255));
cube.setStrokeWeight(weight);
}
void draw() {
background(0);
translate(width/2, height/2);
rotateX(angle);
rotateY(angle);
shader(lineShader, LINES);
shape(cube);
angle += 0.01;
}
linevert.glsl:
uniform mat4 transform;
uniform vec4 viewport;
attribute vec4 position;
attribute vec4 color;
attribute vec4 direction;
varying vec4 vertColor;
vec3 clipToWindow(vec4 clip, vec4 viewport) {
vec3 dclip = clip.xyz / clip.w;
vec2 xypos = (dclip.xy + vec2(1.0, 1.0)) * 0.5 * viewport.zw;
return vec3(xypos, dclip.z * 0.5 + 0.5);
}
void main() {
vec4 clip0 = transform * position;
vec4 clip1 = clip0 + transform * vec4(direction.xyz, 0);
float thickness = direction.w;
vec3 win0 = clipToWindow(clip0, viewport);
vec3 win1 = clipToWindow(clip1, viewport);
vec2 tangent = win1.xy - win0.xy;
vec2 normal = normalize(vec2(-tangent.y, tangent.x));
vec2 offset = normal * thickness;
gl_Position.xy = clip0.xy + offset.xy;
gl_Position.zw = clip0.zw;
vertColor = color;
}
linefrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
varying vec4 vertColor;
void main() {
gl_FragColor = vertColor;
}
顶点着色器中的 clipToWindow() 函数将剪辑参数转换为像素单位的视口坐标,以确保切线向量有效地包含在屏幕中。另请注意,直线四边形中相对点的位置是如何根据当前点和方向向量计算的:
vec4 clip1 = clip0 + transform * vec4(direction.xyz, 0);
本节中的最后一个示例修改了简单的线着色器,以将透明度纳入笔触,这样,随着像素远离笔画的中心脊柱,alpha 值会逐渐减小:
代码清单 10.5: 线条着色器在笔触的边缘具有透明度效果。
PShape cube;
PShader lineShader;
float angle;
float weight = 20;
void setup() {
size(640, 360, P3D);
lineShader = loadShader("linefrag.glsl", "linevert.glsl");
lineShader.set("weight", weight);
cube = createShape(BOX, 150);
cube.setFill(false);
cube.setStroke(color(255));
cube.setStrokeWeight(weight);
hint(DISABLE_DEPTH_MASK);
}
void draw() {
background(0);
translate(width/2, height/2);
rotateX(angle);
rotateY(angle);
shader(lineShader, LINES);
shape(cube);
angle += 0.01;
}
linevert.glsl:
uniform mat4 transform;
uniform vec4 viewport;
attribute vec4 position;
attribute vec4 color;
attribute vec4 direction;
varying vec2 center;
varying vec2 normal;
varying vec4 vertColor;
vec3 clipToWindow(vec4 clip, vec4 viewport) {
vec3 dclip = clip.xyz / clip.w;
vec2 xypos = (dclip.xy + vec2(1.0, 1.0)) * 0.5 * viewport.zw;
return vec3(xypos, dclip.z * 0.5 + 0.5);
}
void main() {
vec4 clip0 = transform * position;
vec4 clip1 = clip0 + transform * vec4(direction.xyz, 0);
float thickness = direction.w;
vec3 win0 = clipToWindow(clip0, viewport);
vec3 win1 = clipToWindow(clip1, viewport);
vec2 tangent = win1.xy - win0.xy;
normal = normalize(vec2(-tangent.y, tangent.x));
vec2 offset = normal * thickness;
gl_Position.xy = clip0.xy + offset.xy;
gl_Position.zw = clip0.zw;
vertColor = color;
center = (win0.xy + win1.xy) / 2.0;
}
linefrag.glsl:
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform float weight;
varying vec2 center;
varying vec2 normal;
varying vec4 vertColor;
void main() {
vec2 v = gl_FragCoord.xy - center;
float alpha = 1.0 - abs(2.0 * dot(normalize(normal), v) / weight);
gl_FragColor = vec4(vertColor.rgb, alpha);
}
这里的关键计算是法线向量和来自笔划四边形中心和当前片段位置的向量之间的点积,点 (归一化 (法线),v),沿着四边形的脊柱正好是 0,因此,给予 alpha 等于 1。
由于使用半透明几何图形和 alpha 混合,草图代码中的禁用深度蒙版提示是一个简单 (但不完美) 的技巧,可以避免明显的视觉伪影。